blob: bb2e27b0350ea1bba1dfbc66f7fbb9fef1e20db0 [file] [log] [blame]
Yilin Yang19da6932019-12-10 13:39:28 +08001#!/usr/bin/env python3
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
Yilin Yangf9fe1932019-11-04 17:09:34 +080033import codecs
Hung-Te Lin138389f2018-05-15 17:55:00 +080034import itertools
Hung-Te Linc772e1a2017-04-14 16:50:50 +080035import logging
36import os
Hung-Te Lin446eb512018-05-02 18:39:16 +080037import stat
Hung-Te Linc772e1a2017-04-14 16:50:50 +080038import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080039import subprocess
40import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080041import uuid
42
43
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080044class StructError(Exception):
45 """Exceptions in packing and unpacking from/to struct fields."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +080046
Hung-Te Linc772e1a2017-04-14 16:50:50 +080047
Fei Shaobd07c9a2020-06-15 19:04:50 +080048class StructField:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080049 """Definition of a field in struct.
50
51 Attributes:
52 fmt: a format string for struct.{pack,unpack} to use.
53 name: a string for name of processed field.
54 """
55 __slots__ = ['fmt', 'name']
56
57 def __init__(self, fmt, name):
58 self.fmt = fmt
59 self.name = name
60
61 def Pack(self, value):
62 """"Packs given value from given format."""
63 del self # Unused.
Yilin Yang235e5982019-12-26 10:36:22 +080064 if isinstance(value, str):
65 value = value.encode('utf-8')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080066 return value
67
68 def Unpack(self, value):
69 """Unpacks given value into given format."""
70 del self # Unused.
71 return value
72
73
74class UTF16StructField(StructField):
75 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080076 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080077 encoding = 'utf-16-le'
78
79 def __init__(self, max_length, name):
80 self.max_length = max_length
81 fmt = '%ds' % max_length
82 super(UTF16StructField, self).__init__(fmt, name)
83
84 def Pack(self, value):
85 new_value = value.encode(self.encoding)
86 if len(new_value) >= self.max_length:
87 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
88 (value, self.name, self.max_length))
89 return new_value
90
91 def Unpack(self, value):
92 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080093
Hung-Te Linbf8aa272018-04-19 03:02:29 +080094
95class GUID(uuid.UUID):
96 """A special UUID that defaults to upper case in str()."""
97
98 def __str__(self):
99 """Returns GUID in upper case."""
100 return super(GUID, self).__str__().upper()
101
102 @staticmethod
103 def Random():
104 return uuid.uuid4()
105
106
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800107class GUIDStructField(StructField):
108 """A GUID field."""
109
110 def __init__(self, name):
111 super(GUIDStructField, self).__init__('16s', name)
112
113 def Pack(self, value):
114 if value is None:
Yilin Yang235e5982019-12-26 10:36:22 +0800115 return b'\x00' * 16
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800116 if not isinstance(value, uuid.UUID):
117 raise StructError('Field %s needs a GUID value instead of [%r].' %
118 (self.name, value))
119 return value.bytes_le
120
121 def Unpack(self, value):
122 return GUID(bytes_le=value)
123
124
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800125def BitProperty(getter, setter, shift, mask):
126 """A generator for bit-field properties.
127
128 This is used inside a class to manipulate an integer-like variable using
129 properties. The getter and setter should be member functions to change the
130 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800131
132 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800133 getter: a function to read integer type variable (for all the bits).
134 setter: a function to set the new changed integer type variable.
135 shift: integer for how many bits should be shifted (right).
136 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800137 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800138 def _getter(self):
139 return (getter(self) >> shift) & mask
140 def _setter(self, value):
141 assert value & mask == value, (
142 'Value %s out of range (mask=%s)' % (value, mask))
143 setter(self, getter(self) & ~(mask << shift) | value << shift)
144 return property(_getter, _setter)
145
146
Fei Shaobd07c9a2020-06-15 19:04:50 +0800147class PartitionAttributes:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800148 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800149
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800150 This can be created using Partition.attrs, but the changed properties won't
151 apply to underlying Partition until an explicit call with
152 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800153 """
154
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800155 def __init__(self, attrs):
156 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800157
158 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800159 def raw(self):
160 """Returns the raw integer type attributes."""
161 return self._Get()
162
163 def _Get(self):
164 return self._attrs
165
166 def _Set(self, value):
167 self._attrs = value
168
169 successful = BitProperty(_Get, _Set, 56, 1)
170 tries = BitProperty(_Get, _Set, 52, 0xf)
171 priority = BitProperty(_Get, _Set, 48, 0xf)
172 legacy_boot = BitProperty(_Get, _Set, 2, 1)
173 required = BitProperty(_Get, _Set, 0, 1)
174 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
175
176
177class PartitionAttributeStructField(StructField):
178
179 def Pack(self, value):
180 if not isinstance(value, PartitionAttributes):
181 raise StructError('Given value %r is not %s.' %
182 (value, PartitionAttributes.__name__))
183 return value.raw
184
185 def Unpack(self, value):
186 return PartitionAttributes(value)
187
188
Yilin Yang9cf532e2019-12-13 12:02:59 +0800189# The binascii.crc32 returns unsigned integer in python3, so CRC32 in struct
190# must be declared as 'unsigned' (L).
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800191# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
192HEADER_FIELDS = [
193 StructField('8s', 'Signature'),
194 StructField('4s', 'Revision'),
195 StructField('L', 'HeaderSize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800196 StructField('L', 'CRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800197 StructField('4s', 'Reserved'),
198 StructField('Q', 'CurrentLBA'),
199 StructField('Q', 'BackupLBA'),
200 StructField('Q', 'FirstUsableLBA'),
201 StructField('Q', 'LastUsableLBA'),
202 GUIDStructField('DiskGUID'),
203 StructField('Q', 'PartitionEntriesStartingLBA'),
204 StructField('L', 'PartitionEntriesNumber'),
205 StructField('L', 'PartitionEntrySize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800206 StructField('L', 'PartitionArrayCRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800207]
208
209# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
210PARTITION_FIELDS = [
211 GUIDStructField('TypeGUID'),
212 GUIDStructField('UniqueGUID'),
213 StructField('Q', 'FirstLBA'),
214 StructField('Q', 'LastLBA'),
215 PartitionAttributeStructField('Q', 'Attributes'),
216 UTF16StructField(72, 'Names'),
217]
218
219# The PMBR has so many variants. The basic format is defined in
220# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
221# implementation, as derived from `cgpt`, is following syslinux as:
222# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
223PMBR_FIELDS = [
224 StructField('424s', 'BootCode'),
225 GUIDStructField('BootGUID'),
226 StructField('L', 'DiskID'),
227 StructField('2s', 'Magic'),
228 StructField('16s', 'LegacyPart0'),
229 StructField('16s', 'LegacyPart1'),
230 StructField('16s', 'LegacyPart2'),
231 StructField('16s', 'LegacyPart3'),
232 StructField('2s', 'Signature'),
233]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800234
235
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800236class GPTError(Exception):
237 """All exceptions by GPT."""
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800238
239
Fei Shaobd07c9a2020-06-15 19:04:50 +0800240class GPTObject:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800241 """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 Yang879fbda2020-05-14 13:52:30 +0800284 for name, value in kargs.items():
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)."""
Fei Shaoa161e752020-06-16 18:14:51 +0800318 metas = set(self.__slots__) - {f.name for f in self.FIELDS}
Fei Shao95075802020-06-16 16:55:25 +0800319 return {name: getattr(self, name) for name in metas}
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800320
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)
Yilin Yang235e5982019-12-26 10:36:22 +0800330 if not isinstance(source, (str, bytes)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800331 return self.Unpack(source.read(struct.calcsize(fmt)))
Yilin Yang235e5982019-12-26 10:36:22 +0800332 if isinstance(source, str):
333 source = source.encode('utf-8')
334 for f, value in zip(self.FIELDS, struct.unpack(fmt.encode('utf-8'),
335 source)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800336 setattr(self, f.name, f.Unpack(value))
Yilin Yang840fdc42020-01-16 16:37:42 +0800337 return None
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800338
339 def Pack(self):
Yilin Yang235e5982019-12-26 10:36:22 +0800340 """Packs values in all fields into a bytes by struct format."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800341 return struct.pack(self.GetStructFormat(),
342 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
343
344 def Clone(self):
345 """Clones a new instance."""
346 return type(self)(*self, **self.meta)
347
348 def Update(self, **dargs):
349 """Applies multiple values in current object."""
Yilin Yang879fbda2020-05-14 13:52:30 +0800350 for name, value in dargs.items():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800351 setattr(self, name, value)
352
353 def Zero(self):
354 """Set all fields to values representing zero or empty.
355
356 Note the meta attributes won't be cleared.
357 """
Fei Shaobd07c9a2020-06-15 19:04:50 +0800358 class ZeroReader:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800359 """A /dev/zero like stream."""
360
361 @staticmethod
362 def read(num):
363 return '\x00' * num
364
365 self.Unpack(ZeroReader())
366
367
Fei Shaobd07c9a2020-06-15 19:04:50 +0800368class GPT:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800369 """A GPT helper class.
370
371 To load GPT from an existing disk image file, use `LoadFromFile`.
372 After modifications were made, use `WriteToFile` to commit changes.
373
374 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800375 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800376 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800377 partitions: a list of GPT partition entry nametuple.
378 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800379 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800380 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800381 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800382 # Old devices uses 'Basic data' type for stateful partition, and newer devices
383 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
384 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800385 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800386 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800387 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
388 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800389 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
390 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
391 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
392 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
393 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800394 }
Fei Shao95075802020-06-16 16:55:25 +0800395 TYPE_GUID_FROM_NAME = {
396 'efi' if v.startswith('EFI') else v.lower().split()[-1]: k
397 for k, v in TYPE_GUID_MAP.items()}
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800398 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
399 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
400 TYPE_GUID_LIST_BOOTABLE = [
401 TYPE_GUID_CHROMEOS_KERNEL,
402 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800403 ]
404
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800405 class ProtectiveMBR(GPTObject):
406 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800407 FIELDS = PMBR_FIELDS
408 __slots__ = [f.name for f in FIELDS]
409
Yilin Yang235e5982019-12-26 10:36:22 +0800410 SIGNATURE = b'\x55\xAA'
411 MAGIC = b'\x1d\x9a'
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800412
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800413 class Header(GPTObject):
414 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800415 FIELDS = HEADER_FIELDS
416 __slots__ = [f.name for f in FIELDS]
417
Yilin Yang235e5982019-12-26 10:36:22 +0800418 SIGNATURES = [b'EFI PART', b'CHROMEOS']
419 SIGNATURE_IGNORE = b'IGNOREME'
420 DEFAULT_REVISION = b'\x00\x00\x01\x00'
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800421
422 DEFAULT_PARTITION_ENTRIES = 128
423 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
424
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800425 @classmethod
426 def Create(cls, size, block_size, pad_blocks=0,
427 part_entries=DEFAULT_PARTITION_ENTRIES):
428 """Creates a header with default values.
429
430 Args:
431 size: integer of expected image size.
432 block_size: integer for size of each block (sector).
433 pad_blocks: number of preserved sectors between header and partitions.
434 part_entries: number of partitions to include in header.
435 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800436 PART_FORMAT = GPT.Partition.GetStructFormat()
437 FORMAT = cls.GetStructFormat()
438 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800439 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
440 parts_bytes = part_entries * part_entry_size
Yilin Yang14d02a22019-11-01 11:32:03 +0800441 parts_blocks = parts_bytes // block_size
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800442 if parts_bytes % block_size:
443 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800444 # CRC32 and PartitionsCRC32 must be updated later explicitly.
445 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800446 Signature=cls.SIGNATURES[0],
447 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800448 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800449 CurrentLBA=1,
Yilin Yang14d02a22019-11-01 11:32:03 +0800450 BackupLBA=size // block_size - 1,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800451 FirstUsableLBA=parts_lba + parts_blocks,
Yilin Yang14d02a22019-11-01 11:32:03 +0800452 LastUsableLBA=size // block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800453 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800454 PartitionEntriesStartingLBA=parts_lba,
455 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800456 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800457
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800458 def UpdateChecksum(self):
459 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800460
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800461 Note the PartitionArrayCRC32 is not touched - you have to make sure that
462 is correct before calling Header.UpdateChecksum().
463 """
464 self.Update(CRC32=0)
465 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800466
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800467 class Partition(GPTObject):
468 """The partition entry in GPT.
469
470 Please include following properties when creating a Partition object:
471 - image: a string for path to the image file the partition maps to.
472 - number: the 1-based partition number.
473 - block_size: an integer for size of each block (LBA, or sector).
474 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800475 FIELDS = PARTITION_FIELDS
476 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800477 NAMES_ENCODING = 'utf-16-le'
478 NAMES_LENGTH = 72
479
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800480 def __str__(self):
481 return '%s#%s' % (self.image, self.number)
482
483 def IsUnused(self):
484 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800485 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800486
Hung-Te Linfe724f82018-04-18 15:03:58 +0800487 def IsChromeOSKernel(self):
488 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800489 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800490
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800491 @property
492 def blocks(self):
493 """Return size of partition in blocks (see block_size)."""
494 return self.LastLBA - self.FirstLBA + 1
495
496 @property
497 def offset(self):
498 """Returns offset to partition in bytes."""
499 return self.FirstLBA * self.block_size
500
501 @property
502 def size(self):
503 """Returns size of partition in bytes."""
504 return self.blocks * self.block_size
505
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800506 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800507 """GPT constructor.
508
509 See LoadFromFile for how it's usually used.
510 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800511 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800512 self.header = None
513 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800514 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800515 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800516
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800517 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800518 def GetTypeGUID(cls, value):
519 """The value may be a GUID in string or a short type string."""
520 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
521 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800522
523 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800524 def Create(cls, image_name, size, block_size, pad_blocks=0):
525 """Creates a new GPT instance from given size and block_size.
526
527 Args:
528 image_name: a string of underlying disk image file name.
529 size: expected size of disk image.
530 block_size: size of each block (sector) in bytes.
531 pad_blocks: number of blocks between header and partitions array.
532 """
533 gpt = cls()
534 gpt.block_size = block_size
535 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
536 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800537 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800538 for i in range(gpt.header.PartitionEntriesNumber)]
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800539 return gpt
540
Hung-Te Lin446eb512018-05-02 18:39:16 +0800541 @staticmethod
542 def IsBlockDevice(image):
543 """Returns if the image is a block device file."""
544 return stat.S_ISBLK(os.stat(image).st_mode)
545
546 @classmethod
547 def GetImageSize(cls, image):
548 """Returns the size of specified image (plain or block device file)."""
549 if not cls.IsBlockDevice(image):
550 return os.path.getsize(image)
551
552 fd = os.open(image, os.O_RDONLY)
553 try:
554 return os.lseek(fd, 0, os.SEEK_END)
555 finally:
556 os.close(fd)
557
558 @classmethod
559 def GetLogicalBlockSize(cls, block_dev):
560 """Returns the logical block (sector) size from a block device file.
561
562 The underlying call is BLKSSZGET. An alternative command is blockdev,
563 but that needs root permission even if we just want to get sector size.
564 """
565 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
566 return int(subprocess.check_output(
567 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
568
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800569 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800570 def LoadFromFile(cls, image):
571 """Loads a GPT table from give disk image file object.
572
573 Args:
574 image: a string as file path or a file-like object to read from.
575 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800576 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800577 with open(image, 'rb') as f:
578 return cls.LoadFromFile(f)
579
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800580 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800581 image.seek(0)
582 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
583 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
584 logging.debug('Found MBR signature in %s', image.name)
585 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
586 logging.debug('Found PMBR in %s', image.name)
587 gpt.pmbr = pmbr
588
Hung-Te Linf148d322018-04-13 10:24:42 +0800589 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800590 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
591 if cls.IsBlockDevice(image.name):
592 block_sizes = [cls.GetLogicalBlockSize(image.name)]
593
594 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800595 # Note because there are devices setting Primary as ignored and the
596 # partition table signature accepts 'CHROMEOS' which is also used by
597 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
598 # first before trying other block sizes, otherwise we may incorrectly
599 # identify a kernel partition as LBA 1 of larger block size system.
600 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
601 (-block_size, os.SEEK_END)]):
602 image.seek(*seek)
603 header = gpt.Header.ReadFrom(image)
604 if header.Signature in cls.Header.SIGNATURES:
605 gpt.block_size = block_size
606 if i != 0:
607 gpt.is_secondary = True
608 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800609 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800610 else:
611 # Nothing found, try next block size.
612 continue
613 # Found a valid signature.
614 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800615 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800616 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800617
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800618 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800619 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800620 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800621 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800622 return p
623
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800624 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800625 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800626 ReadPartition(image, i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800627 for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800628 return gpt
629
Hung-Te Linc5196682018-04-18 22:59:59 +0800630 def GetUsedPartitions(self):
631 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800632
Hung-Te Linc5196682018-04-18 22:59:59 +0800633 Use 'number' property to find the real location of partition in
634 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800635 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800636 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800637
638 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800639 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800640 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800641 return (max(p.LastLBA for p in parts)
642 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800643
644 def GetPartitionTableBlocks(self, header=None):
645 """Returns the blocks (or LBA) of partition table from given header."""
646 if header is None:
647 header = self.header
648 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Yilin Yang14d02a22019-11-01 11:32:03 +0800649 blocks = size // self.block_size
Hung-Te Linf148d322018-04-13 10:24:42 +0800650 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800651 blocks += 1
652 return blocks
653
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800654 def GetPartition(self, number):
655 """Gets the Partition by given (1-based) partition number.
656
657 Args:
658 number: an integer as 1-based partition number.
659 """
660 if not 0 < number <= len(self.partitions):
661 raise GPTError('Invalid partition number %s.' % number)
662 return self.partitions[number - 1]
663
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800664 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800665 """Updates the entry in partition table by given Partition object.
666
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800667 Usually you only need to call this if you want to copy one partition to
668 different location (number of image).
669
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800670 Args:
671 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800672 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800673 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800674 ref = self.partitions[number - 1]
675 part = part.Clone()
676 part.number = number
677 part.image = ref.image
678 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800679 self.partitions[number - 1] = part
680
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800681 def GetSize(self):
682 return self.block_size * (self.header.BackupLBA + 1)
683
684 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800685 """Adjust GPT for a disk image in given size.
686
687 Args:
688 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800689 check_overlap: Checks if the backup partition table overlaps used
690 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800691 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800692 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800693 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800694 raise GPTError(
695 'New file size %d is not valid for image files.' % new_size)
Yilin Yang14d02a22019-11-01 11:32:03 +0800696 new_blocks = new_size // self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800697 if old_size != new_size:
Yilin Yang9881b1e2019-12-11 11:47:33 +0800698 logging.warning('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
699 new_size, new_blocks, old_size,
700 old_size // self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800701 else:
702 logging.info('Image size (%d, LBA=%d) not changed.',
703 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800704 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800705
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800706 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800707 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800708 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800709
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800710 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800711 max_used_lba = self.GetMaxUsedLBA()
712 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800713 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800714
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800715 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800716
717 def GetFreeSpace(self):
718 """Returns the free (available) space left according to LastUsableLBA."""
719 max_lba = self.GetMaxUsedLBA()
720 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800721 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800722
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800723 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800724 """Expands a given partition to last usable LBA.
725
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800726 The size of the partition can actually be reduced if the last usable LBA
727 decreases.
728
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800729 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800730 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800731
732 Returns:
733 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800734 """
735 # Assume no partitions overlap, we need to make sure partition[i] has
736 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800737 p = self.GetPartition(number)
738 if p.IsUnused():
739 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800740 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800741 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800742 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800743 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800744 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800745
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800746 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800747 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800748 new_blocks = p.blocks
Yilin Yang9881b1e2019-12-11 11:47:33 +0800749 logging.warning(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800750 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800751 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800752
Hung-Te Lin3b491672018-04-19 01:41:20 +0800753 def CheckIntegrity(self):
754 """Checks if the GPT objects all look good."""
755 # Check if the header allocation looks good. CurrentLBA and
756 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
757 # LastUsableLBA].
758 header = self.header
759 entries_first_lba = header.PartitionEntriesStartingLBA
760 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
761
762 def CheckOutsideUsable(name, lba, outside_entries=False):
763 if lba < 1:
764 raise GPTError('%s should not live in LBA %s.' % (name, lba))
765 if lba > max(header.BackupLBA, header.CurrentLBA):
766 # Note this is "in theory" possible, but we want to report this as
767 # error as well, since it usually leads to error.
768 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
769 (name, lba, header.BackupLBA))
770 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
771 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
772 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
773 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
774 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
775 (name, lba, entries_first_lba, entries_last_lba))
776 CheckOutsideUsable('Header', header.CurrentLBA, True)
777 CheckOutsideUsable('Backup header', header.BackupLBA, True)
778 CheckOutsideUsable('Partition entries', entries_first_lba)
779 CheckOutsideUsable('Partition entries end', entries_last_lba)
780
781 parts = self.GetUsedPartitions()
782 # Check if partition entries overlap with each other.
783 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
784 lba_list.sort(key=lambda t: t[0])
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800785 for i in range(len(lba_list) - 1):
Hung-Te Lin3b491672018-04-19 01:41:20 +0800786 if lba_list[i][1] >= lba_list[i + 1][0]:
787 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
788 (lba_list[i] + lba_list[i + 1]))
789 # Now, check the first and last partition.
790 if lba_list:
791 p = lba_list[0][2]
792 if p.FirstLBA < header.FirstUsableLBA:
793 raise GPTError(
794 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
795 (p, p.FirstLBA, header.FirstLBA))
796 p = lba_list[-1][2]
797 if p.LastLBA > header.LastUsableLBA:
798 raise GPTError(
799 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
800 (p, p.LastLBA, header.LastLBA))
801 # Check if UniqueGUIDs are not unique.
802 if len(set(p.UniqueGUID for p in parts)) != len(parts):
803 raise GPTError('Partition UniqueGUIDs are duplicated.')
804 # Check if CRCs match.
Yilin Yang235e5982019-12-26 10:36:22 +0800805 if (binascii.crc32(b''.join(p.blob for p in self.partitions)) !=
Hung-Te Lin3b491672018-04-19 01:41:20 +0800806 header.PartitionArrayCRC32):
807 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800808 header_crc = header.Clone()
809 header_crc.UpdateChecksum()
810 if header_crc.CRC32 != header.CRC32:
811 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800812
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800813 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800814 """Updates all checksum fields in GPT objects."""
Yilin Yang235e5982019-12-26 10:36:22 +0800815 parts = b''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800816 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
817 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800818
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800819 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800820 """Returns the backup header according to given header.
821
822 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
823 fields.
824 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800825 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800826 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800827 h = header.Clone()
828 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800829 BackupLBA=header.CurrentLBA,
830 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800831 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800832 h.UpdateChecksum()
833 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800834
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800835 @classmethod
836 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
837 """Writes a protective MBR to given file.
838
839 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
840 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
841 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
842 we can follow that.
843
844 Args:
845 create: True to re-create PMBR structure.
846 bootcode: a blob of new boot code.
847 boot_guid a blob for new boot GUID.
848
849 Returns:
850 The written PMBR structure.
851 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800852 if isinstance(image, str):
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800853 with open(image, 'rb+') as f:
854 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
855
856 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800857 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
858 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800859 pmbr = cls.ProtectiveMBR.ReadFrom(image)
860
861 if create:
862 legacy_sectors = min(
863 0x100000000,
Yilin Yang14d02a22019-11-01 11:32:03 +0800864 GPT.GetImageSize(image.name) // cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800865 # Partition 0 must have have the fixed CHS with number of sectors
866 # (calculated as legacy_sectors later).
Yilin Yangf9fe1932019-11-04 17:09:34 +0800867 part0 = (codecs.decode('00000200eeffffff01000000', 'hex') +
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800868 struct.pack('<I', legacy_sectors))
869 # Partition 1~3 should be all zero.
870 part1 = '\x00' * 16
871 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800872 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800873 BootGUID=cls.TYPE_GUID_UNUSED,
874 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800875 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800876 LegacyPart0=part0,
877 LegacyPart1=part1,
878 LegacyPart2=part1,
879 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800880 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800881
882 if bootcode:
883 if len(bootcode) > len(pmbr.BootCode):
884 logging.info(
885 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
886 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800887 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800888 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800889 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800890
891 blob = pmbr.blob
892 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
893 image.seek(0)
894 image.write(blob)
895 return pmbr
896
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800897 def WriteToFile(self, image):
898 """Updates partition table in a disk image file.
899
900 Args:
901 image: a string as file path or a file-like object to write into.
902 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800903 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800904 with open(image, 'rb+') as f:
905 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800906
907 def WriteData(name, blob, lba):
908 """Writes a blob into given location."""
909 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800910 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800911 image.seek(lba * self.block_size)
912 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800913
914 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800915 self.CheckIntegrity()
Yilin Yang235e5982019-12-26 10:36:22 +0800916 parts_blob = b''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800917
918 header = self.header
919 WriteData('GPT Header', header.blob, header.CurrentLBA)
920 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
921 logging.info(
922 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
923 header.LastUsableLBA)
924
925 if not self.is_secondary:
926 # When is_secondary is True, the header we have is actually backup header.
927 backup_header = self.GetBackupHeader(self.header)
928 WriteData(
929 'Backup Partitions', parts_blob,
930 backup_header.PartitionEntriesStartingLBA)
931 WriteData(
932 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Yilin Yang840fdc42020-01-16 16:37:42 +0800933 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800934
935
Fei Shaobd07c9a2020-06-15 19:04:50 +0800936class GPTCommands:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800937 """Collection of GPT sub commands for command line to use.
938
939 The commands are derived from `cgpt`, but not necessary to be 100% compatible
940 with cgpt.
941 """
942
943 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800944 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800945 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800946 ('type', 'type guid'),
947 ('unique', 'unique guid'),
948 ('label', 'label'),
949 ('Successful', 'Successful flag'),
950 ('Tries', 'Tries flag'),
951 ('Priority', 'Priority flag'),
952 ('Legacy', 'Legacy Boot flag'),
953 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800954
955 def __init__(self):
Fei Shao95075802020-06-16 16:55:25 +0800956 commands = {
957 command.lower(): getattr(self, command)()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800958 for command in dir(self)
959 if (isinstance(getattr(self, command), type) and
960 issubclass(getattr(self, command), self.SubCommand) and
961 getattr(self, command) is not self.SubCommand)
Fei Shao95075802020-06-16 16:55:25 +0800962 }
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800963 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800964
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800965 def DefineArgs(self, parser):
966 """Defines all available commands to an argparser subparsers instance."""
967 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
Yilin Yang879fbda2020-05-14 13:52:30 +0800968 for name, instance in sorted(self.commands.items()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800969 parser = subparsers.add_parser(
970 name, description=instance.__doc__,
971 formatter_class=argparse.RawDescriptionHelpFormatter,
972 help=instance.__doc__.splitlines()[0])
973 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800974
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800975 def Execute(self, args):
976 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800977 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800978
Fei Shaobd07c9a2020-06-15 19:04:50 +0800979 class SubCommand:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800980 """A base class for sub commands to derive from."""
981
982 def DefineArgs(self, parser):
983 """Defines command line arguments to argparse parser.
984
985 Args:
986 parser: An argparse parser instance.
987 """
988 del parser # Unused.
989 raise NotImplementedError
990
991 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800992 """Execute the command with parsed arguments.
993
994 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800995
996 Args:
997 args: An argparse parsed namespace.
998 """
999 del args # Unused.
1000 raise NotImplementedError
1001
Hung-Te Line0d1fa72018-05-15 00:04:48 +08001002 def ExecuteCommandLine(self, *args):
1003 """Execute as invoked from command line.
1004
1005 This provides an easy way to execute particular sub command without
1006 creating argument parser explicitly.
1007
1008 Args:
1009 args: a list of string type command line arguments.
1010 """
1011 parser = argparse.ArgumentParser()
1012 self.DefineArgs(parser)
1013 return self.Execute(parser.parse_args(args))
1014
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001015 class Create(SubCommand):
1016 """Create or reset GPT headers and tables.
1017
1018 Create or reset an empty GPT.
1019 """
1020
1021 def DefineArgs(self, parser):
1022 parser.add_argument(
1023 '-z', '--zero', action='store_true',
1024 help='Zero the sectors of the GPT table and entries')
1025 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001026 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001027 help=('Size (in blocks) of the disk to pad between the '
1028 'primary GPT header and its entries, default %(default)s'))
1029 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001030 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001031 help='Size of each block (sector) in bytes.')
1032 parser.add_argument(
1033 'image_file', type=argparse.FileType('rb+'),
1034 help='Disk image file to create.')
1035
1036 def Execute(self, args):
1037 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001038 if block_size is None:
1039 if GPT.IsBlockDevice(args.image_file.name):
1040 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1041 else:
1042 block_size = GPT.DEFAULT_BLOCK_SIZE
1043
1044 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1045 logging.info('Block (sector) size for %s is set to %s bytes.',
1046 args.image_file.name, block_size)
1047
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001048 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001049 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001050 block_size, args.pad_blocks)
1051 if args.zero:
1052 # In theory we only need to clear LBA 1, but to make sure images already
1053 # initialized with different block size won't have GPT signature in
1054 # different locations, we should zero until first usable LBA.
1055 args.image_file.seek(0)
Yilin Yang235e5982019-12-26 10:36:22 +08001056 args.image_file.write(b'\0' * block_size * gpt.header.FirstUsableLBA)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001057 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001058 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001059 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001060
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001061 class Boot(SubCommand):
1062 """Edit the PMBR sector for legacy BIOSes.
1063
1064 With no options, it will just print the PMBR boot guid.
1065 """
1066
1067 def DefineArgs(self, parser):
1068 parser.add_argument(
1069 '-i', '--number', type=int,
1070 help='Set bootable partition')
1071 parser.add_argument(
Stimim Chen0e6071b2020-04-28 18:08:49 +08001072 '-b', '--bootloader', type=argparse.FileType('rb'),
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001073 help='Install bootloader code in the PMBR')
1074 parser.add_argument(
1075 '-p', '--pmbr', action='store_true',
1076 help='Create legacy PMBR partition table')
1077 parser.add_argument(
1078 'image_file', type=argparse.FileType('rb+'),
1079 help='Disk image file to change PMBR.')
1080
1081 def Execute(self, args):
1082 """Rebuilds the protective MBR."""
1083 bootcode = args.bootloader.read() if args.bootloader else None
1084 boot_guid = None
1085 if args.number is not None:
1086 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001087 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001088 pmbr = GPT.WriteProtectiveMBR(
1089 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1090
You-Cheng Syufff7f422018-05-14 15:37:39 +08001091 print(pmbr.BootGUID)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001092 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001093 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001094
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001095 class Legacy(SubCommand):
1096 """Switch between GPT and Legacy GPT.
1097
1098 Switch GPT header signature to "CHROMEOS".
1099 """
1100
1101 def DefineArgs(self, parser):
1102 parser.add_argument(
1103 '-e', '--efi', action='store_true',
1104 help='Switch GPT header signature back to "EFI PART"')
1105 parser.add_argument(
1106 '-p', '--primary-ignore', action='store_true',
1107 help='Switch primary GPT header signature to "IGNOREME"')
1108 parser.add_argument(
1109 'image_file', type=argparse.FileType('rb+'),
1110 help='Disk image file to change.')
1111
1112 def Execute(self, args):
1113 gpt = GPT.LoadFromFile(args.image_file)
1114 # cgpt behavior: if -p is specified, -e is ignored.
1115 if args.primary_ignore:
1116 if gpt.is_secondary:
1117 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1118 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1119 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1120 gpt.header = gpt.GetBackupHeader(self.header)
1121 gpt.is_secondary = True
1122 else:
1123 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001124 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001125 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001126 args.image_file.close()
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001127 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001128 return ('Set %s primary GPT header to %s.' %
1129 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001130 return ('Changed GPT signature for %s to %s.' %
1131 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001132
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001133 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001134 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001135
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001136 def DefineArgs(self, parser):
1137 parser.add_argument(
1138 'image_file', type=argparse.FileType('rb+'),
1139 help='Disk image file to repair.')
1140
1141 def Execute(self, args):
1142 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001143 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001144 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001145 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001146 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001147
1148 class Expand(SubCommand):
1149 """Expands a GPT partition to all available free space."""
1150
1151 def DefineArgs(self, parser):
1152 parser.add_argument(
1153 '-i', '--number', type=int, required=True,
1154 help='The partition to expand.')
1155 parser.add_argument(
1156 'image_file', type=argparse.FileType('rb+'),
1157 help='Disk image file to modify.')
1158
1159 def Execute(self, args):
1160 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001161 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001162 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001163 args.image_file.close()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001164 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001165 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001166 'Partition %s on disk image file %s has been extended '
1167 'from %s to %s .' %
1168 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1169 new_blocks * gpt.block_size))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001170 return ('Nothing to expand for disk image %s partition %s.' %
1171 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001172
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001173 class Add(SubCommand):
1174 """Add, edit, or remove a partition entry.
1175
1176 Use the -i option to modify an existing partition.
1177 The -b, -s, and -t options must be given for new partitions.
1178
1179 The partition type may also be given as one of these aliases:
1180
1181 firmware ChromeOS firmware
1182 kernel ChromeOS kernel
1183 rootfs ChromeOS rootfs
1184 data Linux data
1185 reserved ChromeOS reserved
1186 efi EFI System Partition
1187 unused Unused (nonexistent) partition
1188 """
1189 def DefineArgs(self, parser):
1190 parser.add_argument(
1191 '-i', '--number', type=int,
1192 help='Specify partition (default is next available)')
1193 parser.add_argument(
1194 '-b', '--begin', type=int,
1195 help='Beginning sector')
1196 parser.add_argument(
1197 '-s', '--sectors', type=int,
1198 help='Size in sectors (logical blocks).')
1199 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001200 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001201 help='Partition Type GUID')
1202 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001203 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001204 help='Partition Unique ID')
1205 parser.add_argument(
1206 '-l', '--label',
1207 help='Label')
1208 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001209 '-S', '--successful', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001210 help='set Successful flag')
1211 parser.add_argument(
1212 '-T', '--tries', type=int,
1213 help='set Tries flag (0-15)')
1214 parser.add_argument(
1215 '-P', '--priority', type=int,
1216 help='set Priority flag (0-15)')
1217 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001218 '-R', '--required', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001219 help='set Required flag')
1220 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001221 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001222 choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001223 help='set Legacy Boot flag')
1224 parser.add_argument(
1225 '-A', '--attribute', dest='raw_16', type=int,
1226 help='set raw 16-bit attribute value (bits 48-63)')
1227 parser.add_argument(
1228 'image_file', type=argparse.FileType('rb+'),
1229 help='Disk image file to modify.')
1230
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001231 def Execute(self, args):
1232 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001233 number = args.number
1234 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001235 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001236
1237 # First and last LBA must be calculated explicitly because the given
1238 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001239 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001240 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001241
1242 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001243 part.Zero()
1244 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001245 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001246 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001247 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001248 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001249
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001250 def UpdateAttr(name):
1251 value = getattr(args, name)
1252 if value is None:
1253 return
1254 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001255
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001256 def GetArg(arg_value, default_value):
1257 return default_value if arg_value is None else arg_value
1258
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001259 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001260 for name in ['legacy_boot', 'required', 'priority', 'tries',
1261 'successful', 'raw_16']:
1262 UpdateAttr(name)
1263 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001264 part.Update(
1265 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001266 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001267 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1268 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1269 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1270 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001271
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001272 # Wipe partition again if it should be empty.
1273 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001274 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001275
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001276 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001277 args.image_file.close()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001278 if part.IsUnused():
1279 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001280 return 'Deleted (zeroed) %s.' % (part,)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001281 return ('%s %s (%s+%s).' %
1282 ('Added' if is_new_part else 'Modified',
1283 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001284
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001285 class Show(SubCommand):
1286 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001287
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001288 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001289 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001290
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001291 def DefineArgs(self, parser):
1292 parser.add_argument(
1293 '--numeric', '-n', action='store_true',
1294 help='Numeric output only.')
1295 parser.add_argument(
1296 '--quick', '-q', action='store_true',
1297 help='Quick output.')
1298 parser.add_argument(
1299 '-i', '--number', type=int,
1300 help='Show specified partition only, with format args.')
1301 for name, help_str in GPTCommands.FORMAT_ARGS:
1302 # TODO(hungte) Alert if multiple args were specified.
1303 parser.add_argument(
1304 '--%s' % name, '-%c' % name[0], action='store_true',
1305 help='[format] %s.' % help_str)
1306 parser.add_argument(
1307 'image_file', type=argparse.FileType('rb'),
1308 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001309
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001310 def Execute(self, args):
1311 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001312
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001313 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001314 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001315 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001316 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001317 if names:
1318 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001319 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001320
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001321 def IsBootableType(guid):
1322 if not guid:
1323 return False
1324 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001325
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001326 def FormatAttribute(attrs, chromeos_kernel=False):
1327 if args.numeric:
1328 return '[%x]' % (attrs.raw >> 48)
1329 results = []
1330 if chromeos_kernel:
1331 results += [
1332 'priority=%d' % attrs.priority,
1333 'tries=%d' % attrs.tries,
1334 'successful=%d' % attrs.successful]
1335 if attrs.required:
1336 results += ['required=1']
1337 if attrs.legacy_boot:
1338 results += ['legacy_boot=1']
1339 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001340
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001341 def ApplyFormatArgs(p):
1342 if args.begin:
1343 return p.FirstLBA
Fei Shao12ecf382020-06-23 18:32:26 +08001344 if args.size:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001345 return p.blocks
Fei Shao12ecf382020-06-23 18:32:26 +08001346 if args.type:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001347 return FormatTypeGUID(p)
Fei Shao12ecf382020-06-23 18:32:26 +08001348 if args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001349 return p.UniqueGUID
Fei Shao12ecf382020-06-23 18:32:26 +08001350 if args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001351 return p.Names
Fei Shao12ecf382020-06-23 18:32:26 +08001352 if args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001353 return p.Attributes.successful
Fei Shao12ecf382020-06-23 18:32:26 +08001354 if args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001355 return p.Attributes.priority
Fei Shao12ecf382020-06-23 18:32:26 +08001356 if args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001357 return p.Attributes.tries
Fei Shao12ecf382020-06-23 18:32:26 +08001358 if args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001359 return p.Attributes.legacy_boot
Fei Shao12ecf382020-06-23 18:32:26 +08001360 if args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001361 return '[%x]' % (p.Attributes.raw >> 48)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001362 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001363
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001364 def IsFormatArgsSpecified():
1365 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001366
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001367 gpt = GPT.LoadFromFile(args.image_file)
1368 logging.debug('%r', gpt.header)
1369 fmt = '%12s %11s %7s %s'
1370 fmt2 = '%32s %s: %s'
1371 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001372
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001373 if IsFormatArgsSpecified() and args.number is None:
1374 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001375
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001376 if not (args.number is None or
1377 0 < args.number <= gpt.header.PartitionEntriesNumber):
1378 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001379
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001380 partitions = gpt.partitions
1381 do_print_gpt_blocks = False
1382 if not (args.quick or IsFormatArgsSpecified()):
1383 print(fmt % header)
1384 if args.number is None:
1385 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001386
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001387 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001388 if gpt.pmbr:
1389 print(fmt % (0, 1, '', 'PMBR'))
1390 if gpt.is_secondary:
1391 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1392 else:
1393 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1394 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1395 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001396
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001397 for p in partitions:
1398 if args.number is None:
1399 # Skip unused partitions.
1400 if p.IsUnused():
1401 continue
1402 elif p.number != args.number:
1403 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001404
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001405 if IsFormatArgsSpecified():
1406 print(ApplyFormatArgs(p))
1407 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001408
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001409 print(fmt % (p.FirstLBA, p.blocks, p.number,
1410 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001411 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001412
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001413 if not args.quick:
1414 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001415 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1416 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001417 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001418 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001419
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001420 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001421 if gpt.is_secondary:
1422 header = gpt.header
1423 else:
1424 f = args.image_file
1425 f.seek(gpt.header.BackupLBA * gpt.block_size)
1426 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001427 print(fmt % (header.PartitionEntriesStartingLBA,
1428 gpt.GetPartitionTableBlocks(header), '',
1429 'Sec GPT table'))
1430 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001431
Hung-Te Lin3b491672018-04-19 01:41:20 +08001432 # Check integrity after showing all fields.
1433 gpt.CheckIntegrity()
1434
Hung-Te Linfe724f82018-04-18 15:03:58 +08001435 class Prioritize(SubCommand):
1436 """Reorder the priority of all kernel partitions.
1437
1438 Reorder the priority of all active ChromeOS Kernel partitions.
1439
1440 With no options this will set the lowest active kernel to priority 1 while
1441 maintaining the original order.
1442 """
1443
1444 def DefineArgs(self, parser):
1445 parser.add_argument(
1446 '-P', '--priority', type=int,
1447 help=('Highest priority to use in the new ordering. '
1448 'The other partitions will be ranked in decreasing '
1449 'priority while preserving their original order. '
1450 'If necessary the lowest ranks will be coalesced. '
1451 'No active kernels will be lowered to priority 0.'))
1452 parser.add_argument(
1453 '-i', '--number', type=int,
1454 help='Specify the partition to make the highest in the new order.')
1455 parser.add_argument(
1456 '-f', '--friends', action='store_true',
1457 help=('Friends of the given partition (those with the same '
1458 'starting priority) are also updated to the new '
1459 'highest priority. '))
1460 parser.add_argument(
1461 'image_file', type=argparse.FileType('rb+'),
1462 help='Disk image file to prioritize.')
1463
1464 def Execute(self, args):
1465 gpt = GPT.LoadFromFile(args.image_file)
1466 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001467 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
Fei Shao95075802020-06-16 16:55:25 +08001468 groups = {k: list(g) for k, g in itertools.groupby(
1469 parts, lambda p: p.Attributes.priority)}
Hung-Te Linfe724f82018-04-18 15:03:58 +08001470 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001471 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001472 if p not in parts:
1473 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001474 pri = p.Attributes.priority
1475 friends = groups.pop(pri)
1476 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001477 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001478 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001479 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001480 groups[new_pri] = [p]
1481 friends.remove(p)
1482 if friends:
1483 groups[pri] = friends
1484
1485 if 0 in groups:
1486 # Do not change any partitions with priority=0
1487 groups.pop(0)
1488
Yilin Yang78fa12e2019-09-25 14:21:10 +08001489 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001490 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001491
1492 # Max priority is 0xf.
1493 highest = min(args.priority or len(prios), 0xf)
1494 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001495
Hung-Te Lin138389f2018-05-15 17:55:00 +08001496 for i, pri in enumerate(prios):
1497 new_priority = max(1, highest - i)
1498 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001499 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001500 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001501 if old_priority == new_priority:
1502 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001503 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001504 if attrs.tries < 1 and not attrs.successful:
1505 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001506 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001507 logging.info('%s priority changed from %s to %s.', p, old_priority,
1508 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001509
1510 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001511 args.image_file.close()
Hung-Te Linfe724f82018-04-18 15:03:58 +08001512
Hung-Te Linf641d302018-04-18 15:09:35 +08001513 class Find(SubCommand):
1514 """Locate a partition by its GUID.
1515
1516 Find a partition by its UUID or label. With no specified DRIVE it scans all
1517 physical drives.
1518
1519 The partition type may also be given as one of these aliases:
1520
1521 firmware ChromeOS firmware
1522 kernel ChromeOS kernel
1523 rootfs ChromeOS rootfs
1524 data Linux data
1525 reserved ChromeOS reserved
1526 efi EFI System Partition
1527 unused Unused (nonexistent) partition
1528 """
1529 def DefineArgs(self, parser):
1530 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001531 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001532 help='Search for Partition Type GUID')
1533 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001534 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001535 help='Search for Partition Unique GUID')
1536 parser.add_argument(
1537 '-l', '--label',
1538 help='Search for Label')
1539 parser.add_argument(
1540 '-n', '--numeric', action='store_true',
1541 help='Numeric output only.')
1542 parser.add_argument(
1543 '-1', '--single-match', action='store_true',
1544 help='Fail if more than one match is found.')
1545 parser.add_argument(
1546 '-M', '--match-file', type=str,
1547 help='Matching partition data must also contain MATCH_FILE content.')
1548 parser.add_argument(
1549 '-O', '--offset', type=int, default=0,
1550 help='Byte offset into partition to match content (default 0).')
1551 parser.add_argument(
1552 'drive', type=argparse.FileType('rb+'), nargs='?',
1553 help='Drive or disk image file to find.')
1554
1555 def Execute(self, args):
1556 if not any((args.type_guid, args.unique_guid, args.label)):
1557 raise GPTError('You must specify at least one of -t, -u, or -l')
1558
1559 drives = [args.drive.name] if args.drive else (
1560 '/dev/%s' % name for name in subprocess.check_output(
Yilin Yang42ba5c62020-05-05 10:32:34 +08001561 'lsblk -d -n -r -o name', shell=True, encoding='utf-8').split())
Hung-Te Linf641d302018-04-18 15:09:35 +08001562
1563 match_pattern = None
1564 if args.match_file:
1565 with open(args.match_file) as f:
1566 match_pattern = f.read()
1567
1568 found = 0
1569 for drive in drives:
1570 try:
1571 gpt = GPT.LoadFromFile(drive)
1572 except GPTError:
1573 if args.drive:
1574 raise
1575 # When scanning all block devices on system, ignore failure.
1576
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001577 def Unmatch(a, b):
1578 return a is not None and a != b
1579
Hung-Te Linf641d302018-04-18 15:09:35 +08001580 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001581 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001582 Unmatch(args.label, p.Names) or
1583 Unmatch(args.unique_guid, p.UniqueGUID) or
1584 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001585 continue
1586 if match_pattern:
1587 with open(drive, 'rb') as f:
1588 f.seek(p.offset + args.offset)
1589 if f.read(len(match_pattern)) != match_pattern:
1590 continue
1591 # Found the partition, now print.
1592 found += 1
1593 if args.numeric:
1594 print(p.number)
1595 else:
1596 # This is actually more for block devices.
1597 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1598 p.number))
1599
1600 if found < 1 or (args.single_match and found > 1):
1601 return 1
1602 return 0
1603
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001604
1605def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001606 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001607 parser = argparse.ArgumentParser(description='GPT Utility.')
1608 parser.add_argument('--verbose', '-v', action='count', default=0,
1609 help='increase verbosity.')
1610 parser.add_argument('--debug', '-d', action='store_true',
1611 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001612 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001613
1614 args = parser.parse_args()
1615 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1616 if args.debug:
1617 log_level = logging.DEBUG
1618 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1619 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001620 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001621 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001622 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001623 sys.exit(code)
Yilin Yang0724c9d2019-11-15 15:53:45 +08001624 elif isinstance(code, str):
Hung-Te Linbad46112018-05-15 16:39:14 +08001625 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001626 except Exception as e:
1627 if args.verbose or args.debug:
1628 logging.exception('Failure in command [%s]', args.command)
Fei Shao9cd93ea2020-06-16 18:31:22 +08001629 sys.exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001630
1631
1632if __name__ == '__main__':
1633 main()