blob: e1d59a735fff3460642c0f578282d0a92f9d2cbb [file] [log] [blame]
Mike Frysinger63bb3c72019-09-01 15:16:26 -04001#!/usr/bin/env python2
Hung-Te Linc772e1a2017-04-14 16:50:50 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""An utility to manipulate GPT on a disk image.
7
8Chromium OS factory software usually needs to access partitions from disk
9images. However, there is no good, lightweight, and portable GPT utility.
10Most Chromium OS systems use `cgpt`, but that's not by default installed on
11Ubuntu. Most systems have parted (GNU) or partx (util-linux-ng) but they have
12their own problems.
13
14For example, when a disk image is resized (usually enlarged for putting more
15resources on stateful partition), GPT table must be updated. However,
16 - `parted` can't repair partition without interactive console in exception
17 handler.
18 - `partx` cannot fix headers nor make changes to partition table.
19 - `cgpt repair` does not fix `LastUsableLBA` so we cannot enlarge partition.
20 - `gdisk` is not installed on most systems.
21
22As a result, we need a dedicated tool to help processing GPT.
23
24This pygpt.py provides a simple and customized implementation for processing
25GPT, as a replacement for `cgpt`.
26"""
27
28
29from __future__ import print_function
30
31import argparse
32import binascii
Hung-Te Lin138389f2018-05-15 17:55:00 +080033import itertools
Hung-Te Linc772e1a2017-04-14 16:50:50 +080034import logging
35import os
Hung-Te Lin446eb512018-05-02 18:39:16 +080036import stat
Hung-Te Linc772e1a2017-04-14 16:50:50 +080037import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080038import subprocess
39import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080040import uuid
41
Yilin Yangea784662019-09-26 13:51:03 +080042from six import iteritems
Hung-Te Linc772e1a2017-04-14 16:50:50 +080043
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080044class StructError(Exception):
45 """Exceptions in packing and unpacking from/to struct fields."""
46 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080047
Hung-Te Linc772e1a2017-04-14 16:50:50 +080048
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080049class StructField(object):
50 """Definition of a field in struct.
51
52 Attributes:
53 fmt: a format string for struct.{pack,unpack} to use.
54 name: a string for name of processed field.
55 """
56 __slots__ = ['fmt', 'name']
57
58 def __init__(self, fmt, name):
59 self.fmt = fmt
60 self.name = name
61
62 def Pack(self, value):
63 """"Packs given value from given format."""
64 del self # Unused.
65 return value
66
67 def Unpack(self, value):
68 """Unpacks given value into given format."""
69 del self # Unused.
70 return value
71
72
73class UTF16StructField(StructField):
74 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080075 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080076 encoding = 'utf-16-le'
77
78 def __init__(self, max_length, name):
79 self.max_length = max_length
80 fmt = '%ds' % max_length
81 super(UTF16StructField, self).__init__(fmt, name)
82
83 def Pack(self, value):
84 new_value = value.encode(self.encoding)
85 if len(new_value) >= self.max_length:
86 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
87 (value, self.name, self.max_length))
88 return new_value
89
90 def Unpack(self, value):
91 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080092
Hung-Te Linbf8aa272018-04-19 03:02:29 +080093
94class GUID(uuid.UUID):
95 """A special UUID that defaults to upper case in str()."""
96
97 def __str__(self):
98 """Returns GUID in upper case."""
99 return super(GUID, self).__str__().upper()
100
101 @staticmethod
102 def Random():
103 return uuid.uuid4()
104
105
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800106class GUIDStructField(StructField):
107 """A GUID field."""
108
109 def __init__(self, name):
110 super(GUIDStructField, self).__init__('16s', name)
111
112 def Pack(self, value):
113 if value is None:
114 return '\x00' * 16
115 if not isinstance(value, uuid.UUID):
116 raise StructError('Field %s needs a GUID value instead of [%r].' %
117 (self.name, value))
118 return value.bytes_le
119
120 def Unpack(self, value):
121 return GUID(bytes_le=value)
122
123
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800124def BitProperty(getter, setter, shift, mask):
125 """A generator for bit-field properties.
126
127 This is used inside a class to manipulate an integer-like variable using
128 properties. The getter and setter should be member functions to change the
129 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800130
131 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800132 getter: a function to read integer type variable (for all the bits).
133 setter: a function to set the new changed integer type variable.
134 shift: integer for how many bits should be shifted (right).
135 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800136 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800137 def _getter(self):
138 return (getter(self) >> shift) & mask
139 def _setter(self, value):
140 assert value & mask == value, (
141 'Value %s out of range (mask=%s)' % (value, mask))
142 setter(self, getter(self) & ~(mask << shift) | value << shift)
143 return property(_getter, _setter)
144
145
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800146class PartitionAttributes(object):
147 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800148
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800149 This can be created using Partition.attrs, but the changed properties won't
150 apply to underlying Partition until an explicit call with
151 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800152 """
153
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800154 def __init__(self, attrs):
155 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800156
157 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800158 def raw(self):
159 """Returns the raw integer type attributes."""
160 return self._Get()
161
162 def _Get(self):
163 return self._attrs
164
165 def _Set(self, value):
166 self._attrs = value
167
168 successful = BitProperty(_Get, _Set, 56, 1)
169 tries = BitProperty(_Get, _Set, 52, 0xf)
170 priority = BitProperty(_Get, _Set, 48, 0xf)
171 legacy_boot = BitProperty(_Get, _Set, 2, 1)
172 required = BitProperty(_Get, _Set, 0, 1)
173 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
174
175
176class PartitionAttributeStructField(StructField):
177
178 def Pack(self, value):
179 if not isinstance(value, PartitionAttributes):
180 raise StructError('Given value %r is not %s.' %
181 (value, PartitionAttributes.__name__))
182 return value.raw
183
184 def Unpack(self, value):
185 return PartitionAttributes(value)
186
187
188# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
189# declared as 'signed' (l) instead of 'unsigned' (L).
190# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
191HEADER_FIELDS = [
192 StructField('8s', 'Signature'),
193 StructField('4s', 'Revision'),
194 StructField('L', 'HeaderSize'),
195 StructField('l', 'CRC32'),
196 StructField('4s', 'Reserved'),
197 StructField('Q', 'CurrentLBA'),
198 StructField('Q', 'BackupLBA'),
199 StructField('Q', 'FirstUsableLBA'),
200 StructField('Q', 'LastUsableLBA'),
201 GUIDStructField('DiskGUID'),
202 StructField('Q', 'PartitionEntriesStartingLBA'),
203 StructField('L', 'PartitionEntriesNumber'),
204 StructField('L', 'PartitionEntrySize'),
205 StructField('l', 'PartitionArrayCRC32'),
206]
207
208# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
209PARTITION_FIELDS = [
210 GUIDStructField('TypeGUID'),
211 GUIDStructField('UniqueGUID'),
212 StructField('Q', 'FirstLBA'),
213 StructField('Q', 'LastLBA'),
214 PartitionAttributeStructField('Q', 'Attributes'),
215 UTF16StructField(72, 'Names'),
216]
217
218# The PMBR has so many variants. The basic format is defined in
219# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
220# implementation, as derived from `cgpt`, is following syslinux as:
221# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
222PMBR_FIELDS = [
223 StructField('424s', 'BootCode'),
224 GUIDStructField('BootGUID'),
225 StructField('L', 'DiskID'),
226 StructField('2s', 'Magic'),
227 StructField('16s', 'LegacyPart0'),
228 StructField('16s', 'LegacyPart1'),
229 StructField('16s', 'LegacyPart2'),
230 StructField('16s', 'LegacyPart3'),
231 StructField('2s', 'Signature'),
232]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800233
234
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800235class GPTError(Exception):
236 """All exceptions by GPT."""
237 pass
238
239
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800240class GPTObject(object):
241 """A base object in GUID Partition Table.
242
243 All objects (for instance, header or partition entries) must inherit this
244 class and define the FIELD attribute with a list of field definitions using
245 StructField.
246
247 The 'name' in StructField will become the attribute name of GPT objects that
248 can be directly packed into / unpacked from. Derived (calculated from existing
249 attributes) attributes should be in lower_case.
250
251 It is also possible to attach some additional properties to the object as meta
252 data (for example path of the underlying image file). To do that, first
253 include it in __slots__ list and specify them as dictionary-type args in
254 constructors. These properties will be preserved when you call Clone().
255
256 To create a new object, call the constructor. Field data can be assigned as
257 in arguments, or give nothing to initialize as zero (see Zero()). Field data
258 and meta values can be also specified in keyword arguments (**kargs) at the
259 same time.
260
261 To read a object from file or stream, use class method ReadFrom(source).
262 To make changes, modify the field directly or use Update(dict), or create a
263 copy by Clone() first then Update.
264
265 To wipe all fields (but not meta), call Zero(). There is currently no way
266 to clear meta except setting them to None one by one.
267 """
268 __slots__ = []
269
Peter Shih533566a2018-09-05 17:48:03 +0800270 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800271 """A list of StructField definitions."""
272
273 def __init__(self, *args, **kargs):
274 if args:
275 if len(args) != len(self.FIELDS):
276 raise GPTError('%s need %s arguments (found %s).' %
277 (type(self).__name__, len(self.FIELDS), len(args)))
278 for f, value in zip(self.FIELDS, args):
279 setattr(self, f.name, value)
280 else:
281 self.Zero()
282
283 all_names = [f for f in self.__slots__]
Yilin Yangea784662019-09-26 13:51:03 +0800284 for name, value in iteritems(kargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800285 if name not in all_names:
286 raise GPTError('%s does not support keyword arg <%s>.' %
287 (type(self).__name__, name))
288 setattr(self, name, value)
289
290 def __iter__(self):
291 """An iterator to return all fields associated in the object."""
292 return (getattr(self, f.name) for f in self.FIELDS)
293
294 def __repr__(self):
295 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800296 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800297
298 @classmethod
299 def GetStructFormat(cls):
300 """Returns a format string for struct to use."""
301 return '<' + ''.join(f.fmt for f in cls.FIELDS)
302
303 @classmethod
304 def ReadFrom(cls, source, **kargs):
305 """Returns an object from given source."""
306 obj = cls(**kargs)
307 obj.Unpack(source)
308 return obj
309
310 @property
311 def blob(self):
312 """The (packed) blob representation of the object."""
313 return self.Pack()
314
315 @property
316 def meta(self):
317 """Meta values (those not in GPT object fields)."""
318 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
319 return dict((name, getattr(self, name)) for name in metas)
320
321 def Unpack(self, source):
322 """Unpacks values from a given source.
323
324 Args:
325 source: a string of bytes or a file-like object to read from.
326 """
327 fmt = self.GetStructFormat()
328 if source is None:
329 source = '\x00' * struct.calcsize(fmt)
330 if not isinstance(source, basestring):
331 return self.Unpack(source.read(struct.calcsize(fmt)))
332 for f, value in zip(self.FIELDS, struct.unpack(fmt, source)):
333 setattr(self, f.name, f.Unpack(value))
334
335 def Pack(self):
336 """Packs values in all fields into a string by struct format."""
337 return struct.pack(self.GetStructFormat(),
338 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
339
340 def Clone(self):
341 """Clones a new instance."""
342 return type(self)(*self, **self.meta)
343
344 def Update(self, **dargs):
345 """Applies multiple values in current object."""
Yilin Yangea784662019-09-26 13:51:03 +0800346 for name, value in iteritems(dargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800347 setattr(self, name, value)
348
349 def Zero(self):
350 """Set all fields to values representing zero or empty.
351
352 Note the meta attributes won't be cleared.
353 """
354 class ZeroReader(object):
355 """A /dev/zero like stream."""
356
357 @staticmethod
358 def read(num):
359 return '\x00' * num
360
361 self.Unpack(ZeroReader())
362
363
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800364class GPT(object):
365 """A GPT helper class.
366
367 To load GPT from an existing disk image file, use `LoadFromFile`.
368 After modifications were made, use `WriteToFile` to commit changes.
369
370 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800371 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800372 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800373 partitions: a list of GPT partition entry nametuple.
374 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800375 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800376 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800377 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800378 # Old devices uses 'Basic data' type for stateful partition, and newer devices
379 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
380 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800381 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800382 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800383 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
384 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800385 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
386 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
387 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
388 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
389 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800390 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800391 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800392 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
Yilin Yangea784662019-09-26 13:51:03 +0800393 for k, v in iteritems(TYPE_GUID_MAP))
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800394 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
395 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
396 TYPE_GUID_LIST_BOOTABLE = [
397 TYPE_GUID_CHROMEOS_KERNEL,
398 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800399 ]
400
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800401 class ProtectiveMBR(GPTObject):
402 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800403 FIELDS = PMBR_FIELDS
404 __slots__ = [f.name for f in FIELDS]
405
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800406 SIGNATURE = '\x55\xAA'
407 MAGIC = '\x1d\x9a'
408
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800409 class Header(GPTObject):
410 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800411 FIELDS = HEADER_FIELDS
412 __slots__ = [f.name for f in FIELDS]
413
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800414 SIGNATURES = ['EFI PART', 'CHROMEOS']
415 SIGNATURE_IGNORE = 'IGNOREME'
416 DEFAULT_REVISION = '\x00\x00\x01\x00'
417
418 DEFAULT_PARTITION_ENTRIES = 128
419 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
420
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800421 @classmethod
422 def Create(cls, size, block_size, pad_blocks=0,
423 part_entries=DEFAULT_PARTITION_ENTRIES):
424 """Creates a header with default values.
425
426 Args:
427 size: integer of expected image size.
428 block_size: integer for size of each block (sector).
429 pad_blocks: number of preserved sectors between header and partitions.
430 part_entries: number of partitions to include in header.
431 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800432 PART_FORMAT = GPT.Partition.GetStructFormat()
433 FORMAT = cls.GetStructFormat()
434 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800435 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
436 parts_bytes = part_entries * part_entry_size
437 parts_blocks = parts_bytes / block_size
438 if parts_bytes % block_size:
439 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800440 # CRC32 and PartitionsCRC32 must be updated later explicitly.
441 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800442 Signature=cls.SIGNATURES[0],
443 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800444 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800445 CurrentLBA=1,
446 BackupLBA=size / block_size - 1,
447 FirstUsableLBA=parts_lba + parts_blocks,
448 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800449 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800450 PartitionEntriesStartingLBA=parts_lba,
451 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800452 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800453
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800454 def UpdateChecksum(self):
455 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800456
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800457 Note the PartitionArrayCRC32 is not touched - you have to make sure that
458 is correct before calling Header.UpdateChecksum().
459 """
460 self.Update(CRC32=0)
461 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800462
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800463 class Partition(GPTObject):
464 """The partition entry in GPT.
465
466 Please include following properties when creating a Partition object:
467 - image: a string for path to the image file the partition maps to.
468 - number: the 1-based partition number.
469 - block_size: an integer for size of each block (LBA, or sector).
470 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800471 FIELDS = PARTITION_FIELDS
472 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800473 NAMES_ENCODING = 'utf-16-le'
474 NAMES_LENGTH = 72
475
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800476 def __str__(self):
477 return '%s#%s' % (self.image, self.number)
478
479 def IsUnused(self):
480 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800481 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800482
Hung-Te Linfe724f82018-04-18 15:03:58 +0800483 def IsChromeOSKernel(self):
484 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800485 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800486
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800487 @property
488 def blocks(self):
489 """Return size of partition in blocks (see block_size)."""
490 return self.LastLBA - self.FirstLBA + 1
491
492 @property
493 def offset(self):
494 """Returns offset to partition in bytes."""
495 return self.FirstLBA * self.block_size
496
497 @property
498 def size(self):
499 """Returns size of partition in bytes."""
500 return self.blocks * self.block_size
501
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800502 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800503 """GPT constructor.
504
505 See LoadFromFile for how it's usually used.
506 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800507 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800508 self.header = None
509 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800510 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800511 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800513 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800514 def GetTypeGUID(cls, value):
515 """The value may be a GUID in string or a short type string."""
516 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
517 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800518
519 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800520 def Create(cls, image_name, size, block_size, pad_blocks=0):
521 """Creates a new GPT instance from given size and block_size.
522
523 Args:
524 image_name: a string of underlying disk image file name.
525 size: expected size of disk image.
526 block_size: size of each block (sector) in bytes.
527 pad_blocks: number of blocks between header and partitions array.
528 """
529 gpt = cls()
530 gpt.block_size = block_size
531 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
532 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800533 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800534 for i in xrange(gpt.header.PartitionEntriesNumber)]
535 return gpt
536
Hung-Te Lin446eb512018-05-02 18:39:16 +0800537 @staticmethod
538 def IsBlockDevice(image):
539 """Returns if the image is a block device file."""
540 return stat.S_ISBLK(os.stat(image).st_mode)
541
542 @classmethod
543 def GetImageSize(cls, image):
544 """Returns the size of specified image (plain or block device file)."""
545 if not cls.IsBlockDevice(image):
546 return os.path.getsize(image)
547
548 fd = os.open(image, os.O_RDONLY)
549 try:
550 return os.lseek(fd, 0, os.SEEK_END)
551 finally:
552 os.close(fd)
553
554 @classmethod
555 def GetLogicalBlockSize(cls, block_dev):
556 """Returns the logical block (sector) size from a block device file.
557
558 The underlying call is BLKSSZGET. An alternative command is blockdev,
559 but that needs root permission even if we just want to get sector size.
560 """
561 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
562 return int(subprocess.check_output(
563 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
564
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800565 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800566 def LoadFromFile(cls, image):
567 """Loads a GPT table from give disk image file object.
568
569 Args:
570 image: a string as file path or a file-like object to read from.
571 """
572 if isinstance(image, basestring):
573 with open(image, 'rb') as f:
574 return cls.LoadFromFile(f)
575
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800576 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800577 image.seek(0)
578 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
579 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
580 logging.debug('Found MBR signature in %s', image.name)
581 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
582 logging.debug('Found PMBR in %s', image.name)
583 gpt.pmbr = pmbr
584
Hung-Te Linf148d322018-04-13 10:24:42 +0800585 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800586 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
587 if cls.IsBlockDevice(image.name):
588 block_sizes = [cls.GetLogicalBlockSize(image.name)]
589
590 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800591 # Note because there are devices setting Primary as ignored and the
592 # partition table signature accepts 'CHROMEOS' which is also used by
593 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
594 # first before trying other block sizes, otherwise we may incorrectly
595 # identify a kernel partition as LBA 1 of larger block size system.
596 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
597 (-block_size, os.SEEK_END)]):
598 image.seek(*seek)
599 header = gpt.Header.ReadFrom(image)
600 if header.Signature in cls.Header.SIGNATURES:
601 gpt.block_size = block_size
602 if i != 0:
603 gpt.is_secondary = True
604 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800605 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800606 else:
607 # Nothing found, try next block size.
608 continue
609 # Found a valid signature.
610 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800611 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800612 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800613
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800614 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800615 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800616 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800617 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800618 return p
619
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800620 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800621 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800622 ReadPartition(image, i + 1)
623 for i in xrange(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800624 return gpt
625
Hung-Te Linc5196682018-04-18 22:59:59 +0800626 def GetUsedPartitions(self):
627 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800628
Hung-Te Linc5196682018-04-18 22:59:59 +0800629 Use 'number' property to find the real location of partition in
630 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800631 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800632 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800633
634 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800635 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800636 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800637 return (max(p.LastLBA for p in parts)
638 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800639
640 def GetPartitionTableBlocks(self, header=None):
641 """Returns the blocks (or LBA) of partition table from given header."""
642 if header is None:
643 header = self.header
644 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800645 blocks = size / self.block_size
646 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800647 blocks += 1
648 return blocks
649
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800650 def GetPartition(self, number):
651 """Gets the Partition by given (1-based) partition number.
652
653 Args:
654 number: an integer as 1-based partition number.
655 """
656 if not 0 < number <= len(self.partitions):
657 raise GPTError('Invalid partition number %s.' % number)
658 return self.partitions[number - 1]
659
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800660 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800661 """Updates the entry in partition table by given Partition object.
662
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800663 Usually you only need to call this if you want to copy one partition to
664 different location (number of image).
665
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800666 Args:
667 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800668 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800669 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800670 ref = self.partitions[number - 1]
671 part = part.Clone()
672 part.number = number
673 part.image = ref.image
674 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800675 self.partitions[number - 1] = part
676
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800677 def GetSize(self):
678 return self.block_size * (self.header.BackupLBA + 1)
679
680 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800681 """Adjust GPT for a disk image in given size.
682
683 Args:
684 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800685 check_overlap: Checks if the backup partition table overlaps used
686 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800687 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800688 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800689 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800690 raise GPTError(
691 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800692 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800693 if old_size != new_size:
694 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800695 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800696 else:
697 logging.info('Image size (%d, LBA=%d) not changed.',
698 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800699 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800700
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800701 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800702 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800703 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800704
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800705 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800706 max_used_lba = self.GetMaxUsedLBA()
707 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800708 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800709
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800710 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800711
712 def GetFreeSpace(self):
713 """Returns the free (available) space left according to LastUsableLBA."""
714 max_lba = self.GetMaxUsedLBA()
715 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800716 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800717
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800718 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800719 """Expands a given partition to last usable LBA.
720
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800721 The size of the partition can actually be reduced if the last usable LBA
722 decreases.
723
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800724 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800725 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800726
727 Returns:
728 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800729 """
730 # Assume no partitions overlap, we need to make sure partition[i] has
731 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800732 p = self.GetPartition(number)
733 if p.IsUnused():
734 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800735 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800736 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800737 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800738 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800739 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800740
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800741 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800742 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800743 new_blocks = p.blocks
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800744 logging.warn(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800745 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800746 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800747
Hung-Te Lin3b491672018-04-19 01:41:20 +0800748 def CheckIntegrity(self):
749 """Checks if the GPT objects all look good."""
750 # Check if the header allocation looks good. CurrentLBA and
751 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
752 # LastUsableLBA].
753 header = self.header
754 entries_first_lba = header.PartitionEntriesStartingLBA
755 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
756
757 def CheckOutsideUsable(name, lba, outside_entries=False):
758 if lba < 1:
759 raise GPTError('%s should not live in LBA %s.' % (name, lba))
760 if lba > max(header.BackupLBA, header.CurrentLBA):
761 # Note this is "in theory" possible, but we want to report this as
762 # error as well, since it usually leads to error.
763 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
764 (name, lba, header.BackupLBA))
765 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
766 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
767 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
768 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
769 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
770 (name, lba, entries_first_lba, entries_last_lba))
771 CheckOutsideUsable('Header', header.CurrentLBA, True)
772 CheckOutsideUsable('Backup header', header.BackupLBA, True)
773 CheckOutsideUsable('Partition entries', entries_first_lba)
774 CheckOutsideUsable('Partition entries end', entries_last_lba)
775
776 parts = self.GetUsedPartitions()
777 # Check if partition entries overlap with each other.
778 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
779 lba_list.sort(key=lambda t: t[0])
780 for i in xrange(len(lba_list) - 1):
781 if lba_list[i][1] >= lba_list[i + 1][0]:
782 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
783 (lba_list[i] + lba_list[i + 1]))
784 # Now, check the first and last partition.
785 if lba_list:
786 p = lba_list[0][2]
787 if p.FirstLBA < header.FirstUsableLBA:
788 raise GPTError(
789 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
790 (p, p.FirstLBA, header.FirstLBA))
791 p = lba_list[-1][2]
792 if p.LastLBA > header.LastUsableLBA:
793 raise GPTError(
794 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
795 (p, p.LastLBA, header.LastLBA))
796 # Check if UniqueGUIDs are not unique.
797 if len(set(p.UniqueGUID for p in parts)) != len(parts):
798 raise GPTError('Partition UniqueGUIDs are duplicated.')
799 # Check if CRCs match.
800 if (binascii.crc32(''.join(p.blob for p in self.partitions)) !=
801 header.PartitionArrayCRC32):
802 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800803 header_crc = header.Clone()
804 header_crc.UpdateChecksum()
805 if header_crc.CRC32 != header.CRC32:
806 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800807
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800808 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800809 """Updates all checksum fields in GPT objects."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800810 parts = ''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800811 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
812 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800813
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800814 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800815 """Returns the backup header according to given header.
816
817 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
818 fields.
819 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800820 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800821 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800822 h = header.Clone()
823 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800824 BackupLBA=header.CurrentLBA,
825 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800826 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800827 h.UpdateChecksum()
828 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800829
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800830 @classmethod
831 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
832 """Writes a protective MBR to given file.
833
834 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
835 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
836 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
837 we can follow that.
838
839 Args:
840 create: True to re-create PMBR structure.
841 bootcode: a blob of new boot code.
842 boot_guid a blob for new boot GUID.
843
844 Returns:
845 The written PMBR structure.
846 """
847 if isinstance(image, basestring):
848 with open(image, 'rb+') as f:
849 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
850
851 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800852 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
853 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800854 pmbr = cls.ProtectiveMBR.ReadFrom(image)
855
856 if create:
857 legacy_sectors = min(
858 0x100000000,
Hung-Te Lin446eb512018-05-02 18:39:16 +0800859 GPT.GetImageSize(image.name) / cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800860 # Partition 0 must have have the fixed CHS with number of sectors
861 # (calculated as legacy_sectors later).
862 part0 = ('00000200eeffffff01000000'.decode('hex') +
863 struct.pack('<I', legacy_sectors))
864 # Partition 1~3 should be all zero.
865 part1 = '\x00' * 16
866 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800867 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800868 BootGUID=cls.TYPE_GUID_UNUSED,
869 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800870 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800871 LegacyPart0=part0,
872 LegacyPart1=part1,
873 LegacyPart2=part1,
874 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800875 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800876
877 if bootcode:
878 if len(bootcode) > len(pmbr.BootCode):
879 logging.info(
880 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
881 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800882 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800883 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800884 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800885
886 blob = pmbr.blob
887 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
888 image.seek(0)
889 image.write(blob)
890 return pmbr
891
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800892 def WriteToFile(self, image):
893 """Updates partition table in a disk image file.
894
895 Args:
896 image: a string as file path or a file-like object to write into.
897 """
898 if isinstance(image, basestring):
899 with open(image, 'rb+') as f:
900 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800901
902 def WriteData(name, blob, lba):
903 """Writes a blob into given location."""
904 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800905 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800906 image.seek(lba * self.block_size)
907 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800908
909 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800910 self.CheckIntegrity()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800911 parts_blob = ''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800912
913 header = self.header
914 WriteData('GPT Header', header.blob, header.CurrentLBA)
915 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
916 logging.info(
917 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
918 header.LastUsableLBA)
919
920 if not self.is_secondary:
921 # When is_secondary is True, the header we have is actually backup header.
922 backup_header = self.GetBackupHeader(self.header)
923 WriteData(
924 'Backup Partitions', parts_blob,
925 backup_header.PartitionEntriesStartingLBA)
926 WriteData(
927 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800928
929
930class GPTCommands(object):
931 """Collection of GPT sub commands for command line to use.
932
933 The commands are derived from `cgpt`, but not necessary to be 100% compatible
934 with cgpt.
935 """
936
937 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800938 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800939 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800940 ('type', 'type guid'),
941 ('unique', 'unique guid'),
942 ('label', 'label'),
943 ('Successful', 'Successful flag'),
944 ('Tries', 'Tries flag'),
945 ('Priority', 'Priority flag'),
946 ('Legacy', 'Legacy Boot flag'),
947 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800948
949 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800950 commands = dict(
951 (command.lower(), getattr(self, command)())
952 for command in dir(self)
953 if (isinstance(getattr(self, command), type) and
954 issubclass(getattr(self, command), self.SubCommand) and
955 getattr(self, command) is not self.SubCommand)
956 )
957 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800958
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800959 def DefineArgs(self, parser):
960 """Defines all available commands to an argparser subparsers instance."""
961 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
Yilin Yangea784662019-09-26 13:51:03 +0800962 for name, instance in sorted(iteritems(self.commands)):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800963 parser = subparsers.add_parser(
964 name, description=instance.__doc__,
965 formatter_class=argparse.RawDescriptionHelpFormatter,
966 help=instance.__doc__.splitlines()[0])
967 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800968
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800969 def Execute(self, args):
970 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800971 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800972
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800973 class SubCommand(object):
974 """A base class for sub commands to derive from."""
975
976 def DefineArgs(self, parser):
977 """Defines command line arguments to argparse parser.
978
979 Args:
980 parser: An argparse parser instance.
981 """
982 del parser # Unused.
983 raise NotImplementedError
984
985 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800986 """Execute the command with parsed arguments.
987
988 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800989
990 Args:
991 args: An argparse parsed namespace.
992 """
993 del args # Unused.
994 raise NotImplementedError
995
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800996 def ExecuteCommandLine(self, *args):
997 """Execute as invoked from command line.
998
999 This provides an easy way to execute particular sub command without
1000 creating argument parser explicitly.
1001
1002 Args:
1003 args: a list of string type command line arguments.
1004 """
1005 parser = argparse.ArgumentParser()
1006 self.DefineArgs(parser)
1007 return self.Execute(parser.parse_args(args))
1008
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001009 class Create(SubCommand):
1010 """Create or reset GPT headers and tables.
1011
1012 Create or reset an empty GPT.
1013 """
1014
1015 def DefineArgs(self, parser):
1016 parser.add_argument(
1017 '-z', '--zero', action='store_true',
1018 help='Zero the sectors of the GPT table and entries')
1019 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001020 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001021 help=('Size (in blocks) of the disk to pad between the '
1022 'primary GPT header and its entries, default %(default)s'))
1023 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001024 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001025 help='Size of each block (sector) in bytes.')
1026 parser.add_argument(
1027 'image_file', type=argparse.FileType('rb+'),
1028 help='Disk image file to create.')
1029
1030 def Execute(self, args):
1031 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001032 if block_size is None:
1033 if GPT.IsBlockDevice(args.image_file.name):
1034 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1035 else:
1036 block_size = GPT.DEFAULT_BLOCK_SIZE
1037
1038 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1039 logging.info('Block (sector) size for %s is set to %s bytes.',
1040 args.image_file.name, block_size)
1041
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001042 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001043 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001044 block_size, args.pad_blocks)
1045 if args.zero:
1046 # In theory we only need to clear LBA 1, but to make sure images already
1047 # initialized with different block size won't have GPT signature in
1048 # different locations, we should zero until first usable LBA.
1049 args.image_file.seek(0)
1050 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
1051 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001052 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001053
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001054 class Boot(SubCommand):
1055 """Edit the PMBR sector for legacy BIOSes.
1056
1057 With no options, it will just print the PMBR boot guid.
1058 """
1059
1060 def DefineArgs(self, parser):
1061 parser.add_argument(
1062 '-i', '--number', type=int,
1063 help='Set bootable partition')
1064 parser.add_argument(
1065 '-b', '--bootloader', type=argparse.FileType('r'),
1066 help='Install bootloader code in the PMBR')
1067 parser.add_argument(
1068 '-p', '--pmbr', action='store_true',
1069 help='Create legacy PMBR partition table')
1070 parser.add_argument(
1071 'image_file', type=argparse.FileType('rb+'),
1072 help='Disk image file to change PMBR.')
1073
1074 def Execute(self, args):
1075 """Rebuilds the protective MBR."""
1076 bootcode = args.bootloader.read() if args.bootloader else None
1077 boot_guid = None
1078 if args.number is not None:
1079 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001080 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001081 pmbr = GPT.WriteProtectiveMBR(
1082 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1083
You-Cheng Syufff7f422018-05-14 15:37:39 +08001084 print(pmbr.BootGUID)
Hung-Te Linbad46112018-05-15 16:39:14 +08001085 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001086
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001087 class Legacy(SubCommand):
1088 """Switch between GPT and Legacy GPT.
1089
1090 Switch GPT header signature to "CHROMEOS".
1091 """
1092
1093 def DefineArgs(self, parser):
1094 parser.add_argument(
1095 '-e', '--efi', action='store_true',
1096 help='Switch GPT header signature back to "EFI PART"')
1097 parser.add_argument(
1098 '-p', '--primary-ignore', action='store_true',
1099 help='Switch primary GPT header signature to "IGNOREME"')
1100 parser.add_argument(
1101 'image_file', type=argparse.FileType('rb+'),
1102 help='Disk image file to change.')
1103
1104 def Execute(self, args):
1105 gpt = GPT.LoadFromFile(args.image_file)
1106 # cgpt behavior: if -p is specified, -e is ignored.
1107 if args.primary_ignore:
1108 if gpt.is_secondary:
1109 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1110 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1111 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1112 gpt.header = gpt.GetBackupHeader(self.header)
1113 gpt.is_secondary = True
1114 else:
1115 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001116 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001117 gpt.WriteToFile(args.image_file)
1118 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001119 return ('Set %s primary GPT header to %s.' %
1120 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001121 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001122 return ('Changed GPT signature for %s to %s.' %
1123 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001124
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001125 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001126 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001127
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001128 def DefineArgs(self, parser):
1129 parser.add_argument(
1130 'image_file', type=argparse.FileType('rb+'),
1131 help='Disk image file to repair.')
1132
1133 def Execute(self, args):
1134 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001135 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001136 gpt.WriteToFile(args.image_file)
Hung-Te Linbad46112018-05-15 16:39:14 +08001137 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001138
1139 class Expand(SubCommand):
1140 """Expands a GPT partition to all available free space."""
1141
1142 def DefineArgs(self, parser):
1143 parser.add_argument(
1144 '-i', '--number', type=int, required=True,
1145 help='The partition to expand.')
1146 parser.add_argument(
1147 'image_file', type=argparse.FileType('rb+'),
1148 help='Disk image file to modify.')
1149
1150 def Execute(self, args):
1151 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001152 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001153 gpt.WriteToFile(args.image_file)
1154 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001155 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001156 'Partition %s on disk image file %s has been extended '
1157 'from %s to %s .' %
1158 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1159 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001160 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001161 return ('Nothing to expand for disk image %s partition %s.' %
1162 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001163
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001164 class Add(SubCommand):
1165 """Add, edit, or remove a partition entry.
1166
1167 Use the -i option to modify an existing partition.
1168 The -b, -s, and -t options must be given for new partitions.
1169
1170 The partition type may also be given as one of these aliases:
1171
1172 firmware ChromeOS firmware
1173 kernel ChromeOS kernel
1174 rootfs ChromeOS rootfs
1175 data Linux data
1176 reserved ChromeOS reserved
1177 efi EFI System Partition
1178 unused Unused (nonexistent) partition
1179 """
1180 def DefineArgs(self, parser):
1181 parser.add_argument(
1182 '-i', '--number', type=int,
1183 help='Specify partition (default is next available)')
1184 parser.add_argument(
1185 '-b', '--begin', type=int,
1186 help='Beginning sector')
1187 parser.add_argument(
1188 '-s', '--sectors', type=int,
1189 help='Size in sectors (logical blocks).')
1190 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001191 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001192 help='Partition Type GUID')
1193 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001194 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001195 help='Partition Unique ID')
1196 parser.add_argument(
1197 '-l', '--label',
1198 help='Label')
1199 parser.add_argument(
1200 '-S', '--successful', type=int, choices=xrange(2),
1201 help='set Successful flag')
1202 parser.add_argument(
1203 '-T', '--tries', type=int,
1204 help='set Tries flag (0-15)')
1205 parser.add_argument(
1206 '-P', '--priority', type=int,
1207 help='set Priority flag (0-15)')
1208 parser.add_argument(
1209 '-R', '--required', type=int, choices=xrange(2),
1210 help='set Required flag')
1211 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001212 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001213 choices=xrange(2),
1214 help='set Legacy Boot flag')
1215 parser.add_argument(
1216 '-A', '--attribute', dest='raw_16', type=int,
1217 help='set raw 16-bit attribute value (bits 48-63)')
1218 parser.add_argument(
1219 'image_file', type=argparse.FileType('rb+'),
1220 help='Disk image file to modify.')
1221
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001222 def Execute(self, args):
1223 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001224 number = args.number
1225 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001226 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001227
1228 # First and last LBA must be calculated explicitly because the given
1229 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001230 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001231 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001232
1233 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001234 part.Zero()
1235 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001236 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001237 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001238 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001239 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001240
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001241 def UpdateAttr(name):
1242 value = getattr(args, name)
1243 if value is None:
1244 return
1245 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001246
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001247 def GetArg(arg_value, default_value):
1248 return default_value if arg_value is None else arg_value
1249
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001250 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001251 for name in ['legacy_boot', 'required', 'priority', 'tries',
1252 'successful', 'raw_16']:
1253 UpdateAttr(name)
1254 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001255 part.Update(
1256 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001257 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001258 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1259 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1260 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1261 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001262
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001263 # Wipe partition again if it should be empty.
1264 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001265 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001266
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001267 gpt.WriteToFile(args.image_file)
1268 if part.IsUnused():
1269 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001270 return 'Deleted (zeroed) %s.' % (part,)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001271 else:
Hung-Te Linbad46112018-05-15 16:39:14 +08001272 return ('%s %s (%s+%s).' %
1273 ('Added' if is_new_part else 'Modified',
1274 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001275
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001276 class Show(SubCommand):
1277 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001278
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001279 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001280 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001281
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001282 def DefineArgs(self, parser):
1283 parser.add_argument(
1284 '--numeric', '-n', action='store_true',
1285 help='Numeric output only.')
1286 parser.add_argument(
1287 '--quick', '-q', action='store_true',
1288 help='Quick output.')
1289 parser.add_argument(
1290 '-i', '--number', type=int,
1291 help='Show specified partition only, with format args.')
1292 for name, help_str in GPTCommands.FORMAT_ARGS:
1293 # TODO(hungte) Alert if multiple args were specified.
1294 parser.add_argument(
1295 '--%s' % name, '-%c' % name[0], action='store_true',
1296 help='[format] %s.' % help_str)
1297 parser.add_argument(
1298 'image_file', type=argparse.FileType('rb'),
1299 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001300
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001301 def Execute(self, args):
1302 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001303
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001304 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001305 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001306 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001307 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001308 if names:
1309 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001310 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001311
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001312 def IsBootableType(guid):
1313 if not guid:
1314 return False
1315 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001316
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001317 def FormatAttribute(attrs, chromeos_kernel=False):
1318 if args.numeric:
1319 return '[%x]' % (attrs.raw >> 48)
1320 results = []
1321 if chromeos_kernel:
1322 results += [
1323 'priority=%d' % attrs.priority,
1324 'tries=%d' % attrs.tries,
1325 'successful=%d' % attrs.successful]
1326 if attrs.required:
1327 results += ['required=1']
1328 if attrs.legacy_boot:
1329 results += ['legacy_boot=1']
1330 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001331
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001332 def ApplyFormatArgs(p):
1333 if args.begin:
1334 return p.FirstLBA
1335 elif args.size:
1336 return p.blocks
1337 elif args.type:
1338 return FormatTypeGUID(p)
1339 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001340 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001341 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001342 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001343 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001344 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001345 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001346 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001347 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001348 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001349 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001350 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001351 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001352 return '[%x]' % (p.Attributes.raw >> 48)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001353 else:
1354 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001355
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001356 def IsFormatArgsSpecified():
1357 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001358
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001359 gpt = GPT.LoadFromFile(args.image_file)
1360 logging.debug('%r', gpt.header)
1361 fmt = '%12s %11s %7s %s'
1362 fmt2 = '%32s %s: %s'
1363 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001364
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001365 if IsFormatArgsSpecified() and args.number is None:
1366 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001367
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001368 if not (args.number is None or
1369 0 < args.number <= gpt.header.PartitionEntriesNumber):
1370 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001371
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001372 partitions = gpt.partitions
1373 do_print_gpt_blocks = False
1374 if not (args.quick or IsFormatArgsSpecified()):
1375 print(fmt % header)
1376 if args.number is None:
1377 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001378
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001379 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001380 if gpt.pmbr:
1381 print(fmt % (0, 1, '', 'PMBR'))
1382 if gpt.is_secondary:
1383 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1384 else:
1385 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1386 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1387 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001388
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001389 for p in partitions:
1390 if args.number is None:
1391 # Skip unused partitions.
1392 if p.IsUnused():
1393 continue
1394 elif p.number != args.number:
1395 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001396
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001397 if IsFormatArgsSpecified():
1398 print(ApplyFormatArgs(p))
1399 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001400
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001401 print(fmt % (p.FirstLBA, p.blocks, p.number,
1402 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001403 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001404
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001405 if not args.quick:
1406 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001407 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1408 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001409 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001410 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001411
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001412 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001413 if gpt.is_secondary:
1414 header = gpt.header
1415 else:
1416 f = args.image_file
1417 f.seek(gpt.header.BackupLBA * gpt.block_size)
1418 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001419 print(fmt % (header.PartitionEntriesStartingLBA,
1420 gpt.GetPartitionTableBlocks(header), '',
1421 'Sec GPT table'))
1422 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001423
Hung-Te Lin3b491672018-04-19 01:41:20 +08001424 # Check integrity after showing all fields.
1425 gpt.CheckIntegrity()
1426
Hung-Te Linfe724f82018-04-18 15:03:58 +08001427 class Prioritize(SubCommand):
1428 """Reorder the priority of all kernel partitions.
1429
1430 Reorder the priority of all active ChromeOS Kernel partitions.
1431
1432 With no options this will set the lowest active kernel to priority 1 while
1433 maintaining the original order.
1434 """
1435
1436 def DefineArgs(self, parser):
1437 parser.add_argument(
1438 '-P', '--priority', type=int,
1439 help=('Highest priority to use in the new ordering. '
1440 'The other partitions will be ranked in decreasing '
1441 'priority while preserving their original order. '
1442 'If necessary the lowest ranks will be coalesced. '
1443 'No active kernels will be lowered to priority 0.'))
1444 parser.add_argument(
1445 '-i', '--number', type=int,
1446 help='Specify the partition to make the highest in the new order.')
1447 parser.add_argument(
1448 '-f', '--friends', action='store_true',
1449 help=('Friends of the given partition (those with the same '
1450 'starting priority) are also updated to the new '
1451 'highest priority. '))
1452 parser.add_argument(
1453 'image_file', type=argparse.FileType('rb+'),
1454 help='Disk image file to prioritize.')
1455
1456 def Execute(self, args):
1457 gpt = GPT.LoadFromFile(args.image_file)
1458 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001459 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1460 groups = dict((k, list(g)) for k, g in itertools.groupby(
1461 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001462 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001463 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001464 if p not in parts:
1465 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001466 pri = p.Attributes.priority
1467 friends = groups.pop(pri)
1468 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001469 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001470 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001471 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001472 groups[new_pri] = [p]
1473 friends.remove(p)
1474 if friends:
1475 groups[pri] = friends
1476
1477 if 0 in groups:
1478 # Do not change any partitions with priority=0
1479 groups.pop(0)
1480
Yilin Yang78fa12e2019-09-25 14:21:10 +08001481 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001482 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001483
1484 # Max priority is 0xf.
1485 highest = min(args.priority or len(prios), 0xf)
1486 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001487
Hung-Te Lin138389f2018-05-15 17:55:00 +08001488 for i, pri in enumerate(prios):
1489 new_priority = max(1, highest - i)
1490 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001491 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001492 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001493 if old_priority == new_priority:
1494 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001495 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001496 if attrs.tries < 1 and not attrs.successful:
1497 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001498 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001499 logging.info('%s priority changed from %s to %s.', p, old_priority,
1500 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001501
1502 gpt.WriteToFile(args.image_file)
1503
Hung-Te Linf641d302018-04-18 15:09:35 +08001504 class Find(SubCommand):
1505 """Locate a partition by its GUID.
1506
1507 Find a partition by its UUID or label. With no specified DRIVE it scans all
1508 physical drives.
1509
1510 The partition type may also be given as one of these aliases:
1511
1512 firmware ChromeOS firmware
1513 kernel ChromeOS kernel
1514 rootfs ChromeOS rootfs
1515 data Linux data
1516 reserved ChromeOS reserved
1517 efi EFI System Partition
1518 unused Unused (nonexistent) partition
1519 """
1520 def DefineArgs(self, parser):
1521 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001522 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001523 help='Search for Partition Type GUID')
1524 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001525 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001526 help='Search for Partition Unique GUID')
1527 parser.add_argument(
1528 '-l', '--label',
1529 help='Search for Label')
1530 parser.add_argument(
1531 '-n', '--numeric', action='store_true',
1532 help='Numeric output only.')
1533 parser.add_argument(
1534 '-1', '--single-match', action='store_true',
1535 help='Fail if more than one match is found.')
1536 parser.add_argument(
1537 '-M', '--match-file', type=str,
1538 help='Matching partition data must also contain MATCH_FILE content.')
1539 parser.add_argument(
1540 '-O', '--offset', type=int, default=0,
1541 help='Byte offset into partition to match content (default 0).')
1542 parser.add_argument(
1543 'drive', type=argparse.FileType('rb+'), nargs='?',
1544 help='Drive or disk image file to find.')
1545
1546 def Execute(self, args):
1547 if not any((args.type_guid, args.unique_guid, args.label)):
1548 raise GPTError('You must specify at least one of -t, -u, or -l')
1549
1550 drives = [args.drive.name] if args.drive else (
1551 '/dev/%s' % name for name in subprocess.check_output(
1552 'lsblk -d -n -r -o name', shell=True).split())
1553
1554 match_pattern = None
1555 if args.match_file:
1556 with open(args.match_file) as f:
1557 match_pattern = f.read()
1558
1559 found = 0
1560 for drive in drives:
1561 try:
1562 gpt = GPT.LoadFromFile(drive)
1563 except GPTError:
1564 if args.drive:
1565 raise
1566 # When scanning all block devices on system, ignore failure.
1567
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001568 def Unmatch(a, b):
1569 return a is not None and a != b
1570
Hung-Te Linf641d302018-04-18 15:09:35 +08001571 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001572 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001573 Unmatch(args.label, p.Names) or
1574 Unmatch(args.unique_guid, p.UniqueGUID) or
1575 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001576 continue
1577 if match_pattern:
1578 with open(drive, 'rb') as f:
1579 f.seek(p.offset + args.offset)
1580 if f.read(len(match_pattern)) != match_pattern:
1581 continue
1582 # Found the partition, now print.
1583 found += 1
1584 if args.numeric:
1585 print(p.number)
1586 else:
1587 # This is actually more for block devices.
1588 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1589 p.number))
1590
1591 if found < 1 or (args.single_match and found > 1):
1592 return 1
1593 return 0
1594
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001595
1596def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001597 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001598 parser = argparse.ArgumentParser(description='GPT Utility.')
1599 parser.add_argument('--verbose', '-v', action='count', default=0,
1600 help='increase verbosity.')
1601 parser.add_argument('--debug', '-d', action='store_true',
1602 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001603 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001604
1605 args = parser.parse_args()
1606 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1607 if args.debug:
1608 log_level = logging.DEBUG
1609 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1610 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001611 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001612 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001613 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001614 sys.exit(code)
Hung-Te Linbad46112018-05-15 16:39:14 +08001615 elif isinstance(code, basestring):
1616 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001617 except Exception as e:
1618 if args.verbose or args.debug:
1619 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001620 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001621
1622
1623if __name__ == '__main__':
1624 main()