blob: 0f1d9a7ff03efbe620870e049e83836d584946ae [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
Yilin Yangea784662019-09-26 13:51:03 +080043from six import iteritems
Yilin Yange6639682019-10-03 12:49:21 +080044
Hung-Te Linc772e1a2017-04-14 16:50:50 +080045
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080046class StructError(Exception):
47 """Exceptions in packing and unpacking from/to struct fields."""
48 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080049
Hung-Te Linc772e1a2017-04-14 16:50:50 +080050
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080051class StructField(object):
52 """Definition of a field in struct.
53
54 Attributes:
55 fmt: a format string for struct.{pack,unpack} to use.
56 name: a string for name of processed field.
57 """
58 __slots__ = ['fmt', 'name']
59
60 def __init__(self, fmt, name):
61 self.fmt = fmt
62 self.name = name
63
64 def Pack(self, value):
65 """"Packs given value from given format."""
66 del self # Unused.
Yilin Yang235e5982019-12-26 10:36:22 +080067 if isinstance(value, str):
68 value = value.encode('utf-8')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080069 return value
70
71 def Unpack(self, value):
72 """Unpacks given value into given format."""
73 del self # Unused.
74 return value
75
76
77class UTF16StructField(StructField):
78 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080079 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080080 encoding = 'utf-16-le'
81
82 def __init__(self, max_length, name):
83 self.max_length = max_length
84 fmt = '%ds' % max_length
85 super(UTF16StructField, self).__init__(fmt, name)
86
87 def Pack(self, value):
88 new_value = value.encode(self.encoding)
89 if len(new_value) >= self.max_length:
90 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
91 (value, self.name, self.max_length))
92 return new_value
93
94 def Unpack(self, value):
95 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080096
Hung-Te Linbf8aa272018-04-19 03:02:29 +080097
98class GUID(uuid.UUID):
99 """A special UUID that defaults to upper case in str()."""
100
101 def __str__(self):
102 """Returns GUID in upper case."""
103 return super(GUID, self).__str__().upper()
104
105 @staticmethod
106 def Random():
107 return uuid.uuid4()
108
109
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800110class GUIDStructField(StructField):
111 """A GUID field."""
112
113 def __init__(self, name):
114 super(GUIDStructField, self).__init__('16s', name)
115
116 def Pack(self, value):
117 if value is None:
Yilin Yang235e5982019-12-26 10:36:22 +0800118 return b'\x00' * 16
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800119 if not isinstance(value, uuid.UUID):
120 raise StructError('Field %s needs a GUID value instead of [%r].' %
121 (self.name, value))
122 return value.bytes_le
123
124 def Unpack(self, value):
125 return GUID(bytes_le=value)
126
127
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800128def BitProperty(getter, setter, shift, mask):
129 """A generator for bit-field properties.
130
131 This is used inside a class to manipulate an integer-like variable using
132 properties. The getter and setter should be member functions to change the
133 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800134
135 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800136 getter: a function to read integer type variable (for all the bits).
137 setter: a function to set the new changed integer type variable.
138 shift: integer for how many bits should be shifted (right).
139 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800140 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800141 def _getter(self):
142 return (getter(self) >> shift) & mask
143 def _setter(self, value):
144 assert value & mask == value, (
145 'Value %s out of range (mask=%s)' % (value, mask))
146 setter(self, getter(self) & ~(mask << shift) | value << shift)
147 return property(_getter, _setter)
148
149
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800150class PartitionAttributes(object):
151 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800152
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800153 This can be created using Partition.attrs, but the changed properties won't
154 apply to underlying Partition until an explicit call with
155 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800156 """
157
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800158 def __init__(self, attrs):
159 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800160
161 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800162 def raw(self):
163 """Returns the raw integer type attributes."""
164 return self._Get()
165
166 def _Get(self):
167 return self._attrs
168
169 def _Set(self, value):
170 self._attrs = value
171
172 successful = BitProperty(_Get, _Set, 56, 1)
173 tries = BitProperty(_Get, _Set, 52, 0xf)
174 priority = BitProperty(_Get, _Set, 48, 0xf)
175 legacy_boot = BitProperty(_Get, _Set, 2, 1)
176 required = BitProperty(_Get, _Set, 0, 1)
177 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
178
179
180class PartitionAttributeStructField(StructField):
181
182 def Pack(self, value):
183 if not isinstance(value, PartitionAttributes):
184 raise StructError('Given value %r is not %s.' %
185 (value, PartitionAttributes.__name__))
186 return value.raw
187
188 def Unpack(self, value):
189 return PartitionAttributes(value)
190
191
Yilin Yang9cf532e2019-12-13 12:02:59 +0800192# The binascii.crc32 returns unsigned integer in python3, so CRC32 in struct
193# must be declared as 'unsigned' (L).
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800194# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
195HEADER_FIELDS = [
196 StructField('8s', 'Signature'),
197 StructField('4s', 'Revision'),
198 StructField('L', 'HeaderSize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800199 StructField('L', 'CRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800200 StructField('4s', 'Reserved'),
201 StructField('Q', 'CurrentLBA'),
202 StructField('Q', 'BackupLBA'),
203 StructField('Q', 'FirstUsableLBA'),
204 StructField('Q', 'LastUsableLBA'),
205 GUIDStructField('DiskGUID'),
206 StructField('Q', 'PartitionEntriesStartingLBA'),
207 StructField('L', 'PartitionEntriesNumber'),
208 StructField('L', 'PartitionEntrySize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800209 StructField('L', 'PartitionArrayCRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800210]
211
212# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
213PARTITION_FIELDS = [
214 GUIDStructField('TypeGUID'),
215 GUIDStructField('UniqueGUID'),
216 StructField('Q', 'FirstLBA'),
217 StructField('Q', 'LastLBA'),
218 PartitionAttributeStructField('Q', 'Attributes'),
219 UTF16StructField(72, 'Names'),
220]
221
222# The PMBR has so many variants. The basic format is defined in
223# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
224# implementation, as derived from `cgpt`, is following syslinux as:
225# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
226PMBR_FIELDS = [
227 StructField('424s', 'BootCode'),
228 GUIDStructField('BootGUID'),
229 StructField('L', 'DiskID'),
230 StructField('2s', 'Magic'),
231 StructField('16s', 'LegacyPart0'),
232 StructField('16s', 'LegacyPart1'),
233 StructField('16s', 'LegacyPart2'),
234 StructField('16s', 'LegacyPart3'),
235 StructField('2s', 'Signature'),
236]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800237
238
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800239class GPTError(Exception):
240 """All exceptions by GPT."""
241 pass
242
243
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800244class GPTObject(object):
245 """A base object in GUID Partition Table.
246
247 All objects (for instance, header or partition entries) must inherit this
248 class and define the FIELD attribute with a list of field definitions using
249 StructField.
250
251 The 'name' in StructField will become the attribute name of GPT objects that
252 can be directly packed into / unpacked from. Derived (calculated from existing
253 attributes) attributes should be in lower_case.
254
255 It is also possible to attach some additional properties to the object as meta
256 data (for example path of the underlying image file). To do that, first
257 include it in __slots__ list and specify them as dictionary-type args in
258 constructors. These properties will be preserved when you call Clone().
259
260 To create a new object, call the constructor. Field data can be assigned as
261 in arguments, or give nothing to initialize as zero (see Zero()). Field data
262 and meta values can be also specified in keyword arguments (**kargs) at the
263 same time.
264
265 To read a object from file or stream, use class method ReadFrom(source).
266 To make changes, modify the field directly or use Update(dict), or create a
267 copy by Clone() first then Update.
268
269 To wipe all fields (but not meta), call Zero(). There is currently no way
270 to clear meta except setting them to None one by one.
271 """
272 __slots__ = []
273
Peter Shih533566a2018-09-05 17:48:03 +0800274 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800275 """A list of StructField definitions."""
276
277 def __init__(self, *args, **kargs):
278 if args:
279 if len(args) != len(self.FIELDS):
280 raise GPTError('%s need %s arguments (found %s).' %
281 (type(self).__name__, len(self.FIELDS), len(args)))
282 for f, value in zip(self.FIELDS, args):
283 setattr(self, f.name, value)
284 else:
285 self.Zero()
286
287 all_names = [f for f in self.__slots__]
Yilin Yangea784662019-09-26 13:51:03 +0800288 for name, value in iteritems(kargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800289 if name not in all_names:
290 raise GPTError('%s does not support keyword arg <%s>.' %
291 (type(self).__name__, name))
292 setattr(self, name, value)
293
294 def __iter__(self):
295 """An iterator to return all fields associated in the object."""
296 return (getattr(self, f.name) for f in self.FIELDS)
297
298 def __repr__(self):
299 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800300 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800301
302 @classmethod
303 def GetStructFormat(cls):
304 """Returns a format string for struct to use."""
305 return '<' + ''.join(f.fmt for f in cls.FIELDS)
306
307 @classmethod
308 def ReadFrom(cls, source, **kargs):
309 """Returns an object from given source."""
310 obj = cls(**kargs)
311 obj.Unpack(source)
312 return obj
313
314 @property
315 def blob(self):
316 """The (packed) blob representation of the object."""
317 return self.Pack()
318
319 @property
320 def meta(self):
321 """Meta values (those not in GPT object fields)."""
322 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
323 return dict((name, getattr(self, name)) for name in metas)
324
325 def Unpack(self, source):
326 """Unpacks values from a given source.
327
328 Args:
329 source: a string of bytes or a file-like object to read from.
330 """
331 fmt = self.GetStructFormat()
332 if source is None:
333 source = '\x00' * struct.calcsize(fmt)
Yilin Yang235e5982019-12-26 10:36:22 +0800334 if not isinstance(source, (str, bytes)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800335 return self.Unpack(source.read(struct.calcsize(fmt)))
Yilin Yang235e5982019-12-26 10:36:22 +0800336 if isinstance(source, str):
337 source = source.encode('utf-8')
338 for f, value in zip(self.FIELDS, struct.unpack(fmt.encode('utf-8'),
339 source)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800340 setattr(self, f.name, f.Unpack(value))
Yilin Yang840fdc42020-01-16 16:37:42 +0800341 return None
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800342
343 def Pack(self):
Yilin Yang235e5982019-12-26 10:36:22 +0800344 """Packs values in all fields into a bytes by struct format."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800345 return struct.pack(self.GetStructFormat(),
346 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
347
348 def Clone(self):
349 """Clones a new instance."""
350 return type(self)(*self, **self.meta)
351
352 def Update(self, **dargs):
353 """Applies multiple values in current object."""
Yilin Yangea784662019-09-26 13:51:03 +0800354 for name, value in iteritems(dargs):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800355 setattr(self, name, value)
356
357 def Zero(self):
358 """Set all fields to values representing zero or empty.
359
360 Note the meta attributes won't be cleared.
361 """
362 class ZeroReader(object):
363 """A /dev/zero like stream."""
364
365 @staticmethod
366 def read(num):
367 return '\x00' * num
368
369 self.Unpack(ZeroReader())
370
371
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800372class GPT(object):
373 """A GPT helper class.
374
375 To load GPT from an existing disk image file, use `LoadFromFile`.
376 After modifications were made, use `WriteToFile` to commit changes.
377
378 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800379 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800380 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800381 partitions: a list of GPT partition entry nametuple.
382 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800383 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800384 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800385 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800386 # Old devices uses 'Basic data' type for stateful partition, and newer devices
387 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
388 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800389 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800390 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800391 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
392 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800393 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
394 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
395 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
396 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
397 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800398 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800399 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800400 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
Yilin Yangea784662019-09-26 13:51:03 +0800401 for k, v in iteritems(TYPE_GUID_MAP))
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800402 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
403 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
404 TYPE_GUID_LIST_BOOTABLE = [
405 TYPE_GUID_CHROMEOS_KERNEL,
406 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800407 ]
408
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800409 class ProtectiveMBR(GPTObject):
410 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800411 FIELDS = PMBR_FIELDS
412 __slots__ = [f.name for f in FIELDS]
413
Yilin Yang235e5982019-12-26 10:36:22 +0800414 SIGNATURE = b'\x55\xAA'
415 MAGIC = b'\x1d\x9a'
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800416
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800417 class Header(GPTObject):
418 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800419 FIELDS = HEADER_FIELDS
420 __slots__ = [f.name for f in FIELDS]
421
Yilin Yang235e5982019-12-26 10:36:22 +0800422 SIGNATURES = [b'EFI PART', b'CHROMEOS']
423 SIGNATURE_IGNORE = b'IGNOREME'
424 DEFAULT_REVISION = b'\x00\x00\x01\x00'
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800425
426 DEFAULT_PARTITION_ENTRIES = 128
427 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
428
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800429 @classmethod
430 def Create(cls, size, block_size, pad_blocks=0,
431 part_entries=DEFAULT_PARTITION_ENTRIES):
432 """Creates a header with default values.
433
434 Args:
435 size: integer of expected image size.
436 block_size: integer for size of each block (sector).
437 pad_blocks: number of preserved sectors between header and partitions.
438 part_entries: number of partitions to include in header.
439 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800440 PART_FORMAT = GPT.Partition.GetStructFormat()
441 FORMAT = cls.GetStructFormat()
442 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800443 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
444 parts_bytes = part_entries * part_entry_size
Yilin Yang14d02a22019-11-01 11:32:03 +0800445 parts_blocks = parts_bytes // block_size
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800446 if parts_bytes % block_size:
447 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800448 # CRC32 and PartitionsCRC32 must be updated later explicitly.
449 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800450 Signature=cls.SIGNATURES[0],
451 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800452 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800453 CurrentLBA=1,
Yilin Yang14d02a22019-11-01 11:32:03 +0800454 BackupLBA=size // block_size - 1,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800455 FirstUsableLBA=parts_lba + parts_blocks,
Yilin Yang14d02a22019-11-01 11:32:03 +0800456 LastUsableLBA=size // block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800457 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800458 PartitionEntriesStartingLBA=parts_lba,
459 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800460 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800461
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800462 def UpdateChecksum(self):
463 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800464
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800465 Note the PartitionArrayCRC32 is not touched - you have to make sure that
466 is correct before calling Header.UpdateChecksum().
467 """
468 self.Update(CRC32=0)
469 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800470
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800471 class Partition(GPTObject):
472 """The partition entry in GPT.
473
474 Please include following properties when creating a Partition object:
475 - image: a string for path to the image file the partition maps to.
476 - number: the 1-based partition number.
477 - block_size: an integer for size of each block (LBA, or sector).
478 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800479 FIELDS = PARTITION_FIELDS
480 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800481 NAMES_ENCODING = 'utf-16-le'
482 NAMES_LENGTH = 72
483
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800484 def __str__(self):
485 return '%s#%s' % (self.image, self.number)
486
487 def IsUnused(self):
488 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800489 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800490
Hung-Te Linfe724f82018-04-18 15:03:58 +0800491 def IsChromeOSKernel(self):
492 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800493 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800494
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800495 @property
496 def blocks(self):
497 """Return size of partition in blocks (see block_size)."""
498 return self.LastLBA - self.FirstLBA + 1
499
500 @property
501 def offset(self):
502 """Returns offset to partition in bytes."""
503 return self.FirstLBA * self.block_size
504
505 @property
506 def size(self):
507 """Returns size of partition in bytes."""
508 return self.blocks * self.block_size
509
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800510 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800511 """GPT constructor.
512
513 See LoadFromFile for how it's usually used.
514 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800515 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800516 self.header = None
517 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800518 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800519 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800520
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800521 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800522 def GetTypeGUID(cls, value):
523 """The value may be a GUID in string or a short type string."""
524 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
525 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800526
527 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800528 def Create(cls, image_name, size, block_size, pad_blocks=0):
529 """Creates a new GPT instance from given size and block_size.
530
531 Args:
532 image_name: a string of underlying disk image file name.
533 size: expected size of disk image.
534 block_size: size of each block (sector) in bytes.
535 pad_blocks: number of blocks between header and partitions array.
536 """
537 gpt = cls()
538 gpt.block_size = block_size
539 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
540 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800541 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800542 for i in range(gpt.header.PartitionEntriesNumber)]
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800543 return gpt
544
Hung-Te Lin446eb512018-05-02 18:39:16 +0800545 @staticmethod
546 def IsBlockDevice(image):
547 """Returns if the image is a block device file."""
548 return stat.S_ISBLK(os.stat(image).st_mode)
549
550 @classmethod
551 def GetImageSize(cls, image):
552 """Returns the size of specified image (plain or block device file)."""
553 if not cls.IsBlockDevice(image):
554 return os.path.getsize(image)
555
556 fd = os.open(image, os.O_RDONLY)
557 try:
558 return os.lseek(fd, 0, os.SEEK_END)
559 finally:
560 os.close(fd)
561
562 @classmethod
563 def GetLogicalBlockSize(cls, block_dev):
564 """Returns the logical block (sector) size from a block device file.
565
566 The underlying call is BLKSSZGET. An alternative command is blockdev,
567 but that needs root permission even if we just want to get sector size.
568 """
569 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
570 return int(subprocess.check_output(
571 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
572
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800573 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800574 def LoadFromFile(cls, image):
575 """Loads a GPT table from give disk image file object.
576
577 Args:
578 image: a string as file path or a file-like object to read from.
579 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800580 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800581 with open(image, 'rb') as f:
582 return cls.LoadFromFile(f)
583
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800584 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800585 image.seek(0)
586 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
587 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
588 logging.debug('Found MBR signature in %s', image.name)
589 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
590 logging.debug('Found PMBR in %s', image.name)
591 gpt.pmbr = pmbr
592
Hung-Te Linf148d322018-04-13 10:24:42 +0800593 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800594 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
595 if cls.IsBlockDevice(image.name):
596 block_sizes = [cls.GetLogicalBlockSize(image.name)]
597
598 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800599 # Note because there are devices setting Primary as ignored and the
600 # partition table signature accepts 'CHROMEOS' which is also used by
601 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
602 # first before trying other block sizes, otherwise we may incorrectly
603 # identify a kernel partition as LBA 1 of larger block size system.
604 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
605 (-block_size, os.SEEK_END)]):
606 image.seek(*seek)
607 header = gpt.Header.ReadFrom(image)
608 if header.Signature in cls.Header.SIGNATURES:
609 gpt.block_size = block_size
610 if i != 0:
611 gpt.is_secondary = True
612 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800613 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800614 else:
615 # Nothing found, try next block size.
616 continue
617 # Found a valid signature.
618 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800619 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800620 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800621
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800622 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800623 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800624 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800625 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800626 return p
627
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800628 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800629 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800630 ReadPartition(image, i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800631 for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800632 return gpt
633
Hung-Te Linc5196682018-04-18 22:59:59 +0800634 def GetUsedPartitions(self):
635 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800636
Hung-Te Linc5196682018-04-18 22:59:59 +0800637 Use 'number' property to find the real location of partition in
638 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800639 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800640 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800641
642 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800643 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800644 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800645 return (max(p.LastLBA for p in parts)
646 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800647
648 def GetPartitionTableBlocks(self, header=None):
649 """Returns the blocks (or LBA) of partition table from given header."""
650 if header is None:
651 header = self.header
652 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Yilin Yang14d02a22019-11-01 11:32:03 +0800653 blocks = size // self.block_size
Hung-Te Linf148d322018-04-13 10:24:42 +0800654 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800655 blocks += 1
656 return blocks
657
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800658 def GetPartition(self, number):
659 """Gets the Partition by given (1-based) partition number.
660
661 Args:
662 number: an integer as 1-based partition number.
663 """
664 if not 0 < number <= len(self.partitions):
665 raise GPTError('Invalid partition number %s.' % number)
666 return self.partitions[number - 1]
667
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800668 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800669 """Updates the entry in partition table by given Partition object.
670
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800671 Usually you only need to call this if you want to copy one partition to
672 different location (number of image).
673
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800674 Args:
675 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800676 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800677 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800678 ref = self.partitions[number - 1]
679 part = part.Clone()
680 part.number = number
681 part.image = ref.image
682 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800683 self.partitions[number - 1] = part
684
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800685 def GetSize(self):
686 return self.block_size * (self.header.BackupLBA + 1)
687
688 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800689 """Adjust GPT for a disk image in given size.
690
691 Args:
692 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800693 check_overlap: Checks if the backup partition table overlaps used
694 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800695 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800696 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800697 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800698 raise GPTError(
699 'New file size %d is not valid for image files.' % new_size)
Yilin Yang14d02a22019-11-01 11:32:03 +0800700 new_blocks = new_size // self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800701 if old_size != new_size:
Yilin Yang9881b1e2019-12-11 11:47:33 +0800702 logging.warning('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
703 new_size, new_blocks, old_size,
704 old_size // self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800705 else:
706 logging.info('Image size (%d, LBA=%d) not changed.',
707 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800708 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800709
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800710 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800711 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800712 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800713
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800714 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800715 max_used_lba = self.GetMaxUsedLBA()
716 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800717 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800718
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800719 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800720
721 def GetFreeSpace(self):
722 """Returns the free (available) space left according to LastUsableLBA."""
723 max_lba = self.GetMaxUsedLBA()
724 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800725 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800726
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800727 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800728 """Expands a given partition to last usable LBA.
729
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800730 The size of the partition can actually be reduced if the last usable LBA
731 decreases.
732
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800733 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800734 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800735
736 Returns:
737 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800738 """
739 # Assume no partitions overlap, we need to make sure partition[i] has
740 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800741 p = self.GetPartition(number)
742 if p.IsUnused():
743 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800744 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800745 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800746 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800747 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800748 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800749
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800750 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800751 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800752 new_blocks = p.blocks
Yilin Yang9881b1e2019-12-11 11:47:33 +0800753 logging.warning(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800754 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800755 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800756
Hung-Te Lin3b491672018-04-19 01:41:20 +0800757 def CheckIntegrity(self):
758 """Checks if the GPT objects all look good."""
759 # Check if the header allocation looks good. CurrentLBA and
760 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
761 # LastUsableLBA].
762 header = self.header
763 entries_first_lba = header.PartitionEntriesStartingLBA
764 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
765
766 def CheckOutsideUsable(name, lba, outside_entries=False):
767 if lba < 1:
768 raise GPTError('%s should not live in LBA %s.' % (name, lba))
769 if lba > max(header.BackupLBA, header.CurrentLBA):
770 # Note this is "in theory" possible, but we want to report this as
771 # error as well, since it usually leads to error.
772 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
773 (name, lba, header.BackupLBA))
774 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
775 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
776 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
777 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
778 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
779 (name, lba, entries_first_lba, entries_last_lba))
780 CheckOutsideUsable('Header', header.CurrentLBA, True)
781 CheckOutsideUsable('Backup header', header.BackupLBA, True)
782 CheckOutsideUsable('Partition entries', entries_first_lba)
783 CheckOutsideUsable('Partition entries end', entries_last_lba)
784
785 parts = self.GetUsedPartitions()
786 # Check if partition entries overlap with each other.
787 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
788 lba_list.sort(key=lambda t: t[0])
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800789 for i in range(len(lba_list) - 1):
Hung-Te Lin3b491672018-04-19 01:41:20 +0800790 if lba_list[i][1] >= lba_list[i + 1][0]:
791 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
792 (lba_list[i] + lba_list[i + 1]))
793 # Now, check the first and last partition.
794 if lba_list:
795 p = lba_list[0][2]
796 if p.FirstLBA < header.FirstUsableLBA:
797 raise GPTError(
798 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
799 (p, p.FirstLBA, header.FirstLBA))
800 p = lba_list[-1][2]
801 if p.LastLBA > header.LastUsableLBA:
802 raise GPTError(
803 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
804 (p, p.LastLBA, header.LastLBA))
805 # Check if UniqueGUIDs are not unique.
806 if len(set(p.UniqueGUID for p in parts)) != len(parts):
807 raise GPTError('Partition UniqueGUIDs are duplicated.')
808 # Check if CRCs match.
Yilin Yang235e5982019-12-26 10:36:22 +0800809 if (binascii.crc32(b''.join(p.blob for p in self.partitions)) !=
Hung-Te Lin3b491672018-04-19 01:41:20 +0800810 header.PartitionArrayCRC32):
811 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800812 header_crc = header.Clone()
813 header_crc.UpdateChecksum()
814 if header_crc.CRC32 != header.CRC32:
815 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800816
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800817 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800818 """Updates all checksum fields in GPT objects."""
Yilin Yang235e5982019-12-26 10:36:22 +0800819 parts = b''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800820 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
821 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800822
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800823 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800824 """Returns the backup header according to given header.
825
826 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
827 fields.
828 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800829 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800830 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800831 h = header.Clone()
832 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800833 BackupLBA=header.CurrentLBA,
834 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800835 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800836 h.UpdateChecksum()
837 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800838
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800839 @classmethod
840 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
841 """Writes a protective MBR to given file.
842
843 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
844 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
845 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
846 we can follow that.
847
848 Args:
849 create: True to re-create PMBR structure.
850 bootcode: a blob of new boot code.
851 boot_guid a blob for new boot GUID.
852
853 Returns:
854 The written PMBR structure.
855 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800856 if isinstance(image, str):
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800857 with open(image, 'rb+') as f:
858 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
859
860 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800861 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
862 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800863 pmbr = cls.ProtectiveMBR.ReadFrom(image)
864
865 if create:
866 legacy_sectors = min(
867 0x100000000,
Yilin Yang14d02a22019-11-01 11:32:03 +0800868 GPT.GetImageSize(image.name) // cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800869 # Partition 0 must have have the fixed CHS with number of sectors
870 # (calculated as legacy_sectors later).
Yilin Yangf9fe1932019-11-04 17:09:34 +0800871 part0 = (codecs.decode('00000200eeffffff01000000', 'hex') +
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800872 struct.pack('<I', legacy_sectors))
873 # Partition 1~3 should be all zero.
874 part1 = '\x00' * 16
875 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800876 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800877 BootGUID=cls.TYPE_GUID_UNUSED,
878 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800879 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800880 LegacyPart0=part0,
881 LegacyPart1=part1,
882 LegacyPart2=part1,
883 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800884 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800885
886 if bootcode:
887 if len(bootcode) > len(pmbr.BootCode):
888 logging.info(
889 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
890 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800891 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800892 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800893 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800894
895 blob = pmbr.blob
896 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
897 image.seek(0)
898 image.write(blob)
899 return pmbr
900
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800901 def WriteToFile(self, image):
902 """Updates partition table in a disk image file.
903
904 Args:
905 image: a string as file path or a file-like object to write into.
906 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800907 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800908 with open(image, 'rb+') as f:
909 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800910
911 def WriteData(name, blob, lba):
912 """Writes a blob into given location."""
913 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800914 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800915 image.seek(lba * self.block_size)
916 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800917
918 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800919 self.CheckIntegrity()
Yilin Yang235e5982019-12-26 10:36:22 +0800920 parts_blob = b''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800921
922 header = self.header
923 WriteData('GPT Header', header.blob, header.CurrentLBA)
924 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
925 logging.info(
926 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
927 header.LastUsableLBA)
928
929 if not self.is_secondary:
930 # When is_secondary is True, the header we have is actually backup header.
931 backup_header = self.GetBackupHeader(self.header)
932 WriteData(
933 'Backup Partitions', parts_blob,
934 backup_header.PartitionEntriesStartingLBA)
935 WriteData(
936 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Yilin Yang840fdc42020-01-16 16:37:42 +0800937 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800938
939
940class GPTCommands(object):
941 """Collection of GPT sub commands for command line to use.
942
943 The commands are derived from `cgpt`, but not necessary to be 100% compatible
944 with cgpt.
945 """
946
947 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800948 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800949 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800950 ('type', 'type guid'),
951 ('unique', 'unique guid'),
952 ('label', 'label'),
953 ('Successful', 'Successful flag'),
954 ('Tries', 'Tries flag'),
955 ('Priority', 'Priority flag'),
956 ('Legacy', 'Legacy Boot flag'),
957 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800958
959 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800960 commands = dict(
961 (command.lower(), getattr(self, command)())
962 for command in dir(self)
963 if (isinstance(getattr(self, command), type) and
964 issubclass(getattr(self, command), self.SubCommand) and
965 getattr(self, command) is not self.SubCommand)
966 )
967 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800968
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800969 def DefineArgs(self, parser):
970 """Defines all available commands to an argparser subparsers instance."""
971 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
Yilin Yangea784662019-09-26 13:51:03 +0800972 for name, instance in sorted(iteritems(self.commands)):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800973 parser = subparsers.add_parser(
974 name, description=instance.__doc__,
975 formatter_class=argparse.RawDescriptionHelpFormatter,
976 help=instance.__doc__.splitlines()[0])
977 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800978
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800979 def Execute(self, args):
980 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800981 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800982
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800983 class SubCommand(object):
984 """A base class for sub commands to derive from."""
985
986 def DefineArgs(self, parser):
987 """Defines command line arguments to argparse parser.
988
989 Args:
990 parser: An argparse parser instance.
991 """
992 del parser # Unused.
993 raise NotImplementedError
994
995 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800996 """Execute the command with parsed arguments.
997
998 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800999
1000 Args:
1001 args: An argparse parsed namespace.
1002 """
1003 del args # Unused.
1004 raise NotImplementedError
1005
Hung-Te Line0d1fa72018-05-15 00:04:48 +08001006 def ExecuteCommandLine(self, *args):
1007 """Execute as invoked from command line.
1008
1009 This provides an easy way to execute particular sub command without
1010 creating argument parser explicitly.
1011
1012 Args:
1013 args: a list of string type command line arguments.
1014 """
1015 parser = argparse.ArgumentParser()
1016 self.DefineArgs(parser)
1017 return self.Execute(parser.parse_args(args))
1018
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001019 class Create(SubCommand):
1020 """Create or reset GPT headers and tables.
1021
1022 Create or reset an empty GPT.
1023 """
1024
1025 def DefineArgs(self, parser):
1026 parser.add_argument(
1027 '-z', '--zero', action='store_true',
1028 help='Zero the sectors of the GPT table and entries')
1029 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001030 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001031 help=('Size (in blocks) of the disk to pad between the '
1032 'primary GPT header and its entries, default %(default)s'))
1033 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001034 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001035 help='Size of each block (sector) in bytes.')
1036 parser.add_argument(
1037 'image_file', type=argparse.FileType('rb+'),
1038 help='Disk image file to create.')
1039
1040 def Execute(self, args):
1041 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001042 if block_size is None:
1043 if GPT.IsBlockDevice(args.image_file.name):
1044 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1045 else:
1046 block_size = GPT.DEFAULT_BLOCK_SIZE
1047
1048 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1049 logging.info('Block (sector) size for %s is set to %s bytes.',
1050 args.image_file.name, block_size)
1051
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001052 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001053 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001054 block_size, args.pad_blocks)
1055 if args.zero:
1056 # In theory we only need to clear LBA 1, but to make sure images already
1057 # initialized with different block size won't have GPT signature in
1058 # different locations, we should zero until first usable LBA.
1059 args.image_file.seek(0)
Yilin Yang235e5982019-12-26 10:36:22 +08001060 args.image_file.write(b'\0' * block_size * gpt.header.FirstUsableLBA)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001061 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001062 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001063 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001064
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001065 class Boot(SubCommand):
1066 """Edit the PMBR sector for legacy BIOSes.
1067
1068 With no options, it will just print the PMBR boot guid.
1069 """
1070
1071 def DefineArgs(self, parser):
1072 parser.add_argument(
1073 '-i', '--number', type=int,
1074 help='Set bootable partition')
1075 parser.add_argument(
Stimim Chen0e6071b2020-04-28 18:08:49 +08001076 '-b', '--bootloader', type=argparse.FileType('rb'),
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001077 help='Install bootloader code in the PMBR')
1078 parser.add_argument(
1079 '-p', '--pmbr', action='store_true',
1080 help='Create legacy PMBR partition table')
1081 parser.add_argument(
1082 'image_file', type=argparse.FileType('rb+'),
1083 help='Disk image file to change PMBR.')
1084
1085 def Execute(self, args):
1086 """Rebuilds the protective MBR."""
1087 bootcode = args.bootloader.read() if args.bootloader else None
1088 boot_guid = None
1089 if args.number is not None:
1090 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001091 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001092 pmbr = GPT.WriteProtectiveMBR(
1093 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1094
You-Cheng Syufff7f422018-05-14 15:37:39 +08001095 print(pmbr.BootGUID)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001096 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001097 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001098
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001099 class Legacy(SubCommand):
1100 """Switch between GPT and Legacy GPT.
1101
1102 Switch GPT header signature to "CHROMEOS".
1103 """
1104
1105 def DefineArgs(self, parser):
1106 parser.add_argument(
1107 '-e', '--efi', action='store_true',
1108 help='Switch GPT header signature back to "EFI PART"')
1109 parser.add_argument(
1110 '-p', '--primary-ignore', action='store_true',
1111 help='Switch primary GPT header signature to "IGNOREME"')
1112 parser.add_argument(
1113 'image_file', type=argparse.FileType('rb+'),
1114 help='Disk image file to change.')
1115
1116 def Execute(self, args):
1117 gpt = GPT.LoadFromFile(args.image_file)
1118 # cgpt behavior: if -p is specified, -e is ignored.
1119 if args.primary_ignore:
1120 if gpt.is_secondary:
1121 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1122 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1123 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1124 gpt.header = gpt.GetBackupHeader(self.header)
1125 gpt.is_secondary = True
1126 else:
1127 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001128 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001129 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001130 args.image_file.close()
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001131 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001132 return ('Set %s primary GPT header to %s.' %
1133 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001134 return ('Changed GPT signature for %s to %s.' %
1135 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001136
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001137 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001138 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001139
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001140 def DefineArgs(self, parser):
1141 parser.add_argument(
1142 'image_file', type=argparse.FileType('rb+'),
1143 help='Disk image file to repair.')
1144
1145 def Execute(self, args):
1146 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001147 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001148 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001149 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001150 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001151
1152 class Expand(SubCommand):
1153 """Expands a GPT partition to all available free space."""
1154
1155 def DefineArgs(self, parser):
1156 parser.add_argument(
1157 '-i', '--number', type=int, required=True,
1158 help='The partition to expand.')
1159 parser.add_argument(
1160 'image_file', type=argparse.FileType('rb+'),
1161 help='Disk image file to modify.')
1162
1163 def Execute(self, args):
1164 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001165 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001166 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001167 args.image_file.close()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001168 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001169 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001170 'Partition %s on disk image file %s has been extended '
1171 'from %s to %s .' %
1172 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1173 new_blocks * gpt.block_size))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001174 return ('Nothing to expand for disk image %s partition %s.' %
1175 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001176
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001177 class Add(SubCommand):
1178 """Add, edit, or remove a partition entry.
1179
1180 Use the -i option to modify an existing partition.
1181 The -b, -s, and -t options must be given for new partitions.
1182
1183 The partition type may also be given as one of these aliases:
1184
1185 firmware ChromeOS firmware
1186 kernel ChromeOS kernel
1187 rootfs ChromeOS rootfs
1188 data Linux data
1189 reserved ChromeOS reserved
1190 efi EFI System Partition
1191 unused Unused (nonexistent) partition
1192 """
1193 def DefineArgs(self, parser):
1194 parser.add_argument(
1195 '-i', '--number', type=int,
1196 help='Specify partition (default is next available)')
1197 parser.add_argument(
1198 '-b', '--begin', type=int,
1199 help='Beginning sector')
1200 parser.add_argument(
1201 '-s', '--sectors', type=int,
1202 help='Size in sectors (logical blocks).')
1203 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001204 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001205 help='Partition Type GUID')
1206 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001207 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001208 help='Partition Unique ID')
1209 parser.add_argument(
1210 '-l', '--label',
1211 help='Label')
1212 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001213 '-S', '--successful', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001214 help='set Successful flag')
1215 parser.add_argument(
1216 '-T', '--tries', type=int,
1217 help='set Tries flag (0-15)')
1218 parser.add_argument(
1219 '-P', '--priority', type=int,
1220 help='set Priority flag (0-15)')
1221 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001222 '-R', '--required', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001223 help='set Required flag')
1224 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001225 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001226 choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001227 help='set Legacy Boot flag')
1228 parser.add_argument(
1229 '-A', '--attribute', dest='raw_16', type=int,
1230 help='set raw 16-bit attribute value (bits 48-63)')
1231 parser.add_argument(
1232 'image_file', type=argparse.FileType('rb+'),
1233 help='Disk image file to modify.')
1234
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001235 def Execute(self, args):
1236 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001237 number = args.number
1238 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001239 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001240
1241 # First and last LBA must be calculated explicitly because the given
1242 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001243 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001244 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001245
1246 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001247 part.Zero()
1248 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001249 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001250 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001251 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001252 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001253
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001254 def UpdateAttr(name):
1255 value = getattr(args, name)
1256 if value is None:
1257 return
1258 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001259
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001260 def GetArg(arg_value, default_value):
1261 return default_value if arg_value is None else arg_value
1262
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001263 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001264 for name in ['legacy_boot', 'required', 'priority', 'tries',
1265 'successful', 'raw_16']:
1266 UpdateAttr(name)
1267 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001268 part.Update(
1269 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001270 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001271 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1272 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1273 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1274 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001275
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001276 # Wipe partition again if it should be empty.
1277 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001278 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001279
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001280 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001281 args.image_file.close()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001282 if part.IsUnused():
1283 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001284 return 'Deleted (zeroed) %s.' % (part,)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001285 return ('%s %s (%s+%s).' %
1286 ('Added' if is_new_part else 'Modified',
1287 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001288
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001289 class Show(SubCommand):
1290 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001291
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001292 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001293 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001294
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001295 def DefineArgs(self, parser):
1296 parser.add_argument(
1297 '--numeric', '-n', action='store_true',
1298 help='Numeric output only.')
1299 parser.add_argument(
1300 '--quick', '-q', action='store_true',
1301 help='Quick output.')
1302 parser.add_argument(
1303 '-i', '--number', type=int,
1304 help='Show specified partition only, with format args.')
1305 for name, help_str in GPTCommands.FORMAT_ARGS:
1306 # TODO(hungte) Alert if multiple args were specified.
1307 parser.add_argument(
1308 '--%s' % name, '-%c' % name[0], action='store_true',
1309 help='[format] %s.' % help_str)
1310 parser.add_argument(
1311 'image_file', type=argparse.FileType('rb'),
1312 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001313
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001314 def Execute(self, args):
1315 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001316
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001317 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001318 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001319 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001320 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001321 if names:
1322 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001323 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001324
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001325 def IsBootableType(guid):
1326 if not guid:
1327 return False
1328 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001329
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001330 def FormatAttribute(attrs, chromeos_kernel=False):
1331 if args.numeric:
1332 return '[%x]' % (attrs.raw >> 48)
1333 results = []
1334 if chromeos_kernel:
1335 results += [
1336 'priority=%d' % attrs.priority,
1337 'tries=%d' % attrs.tries,
1338 'successful=%d' % attrs.successful]
1339 if attrs.required:
1340 results += ['required=1']
1341 if attrs.legacy_boot:
1342 results += ['legacy_boot=1']
1343 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001344
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001345 def ApplyFormatArgs(p):
1346 if args.begin:
1347 return p.FirstLBA
1348 elif args.size:
1349 return p.blocks
1350 elif args.type:
1351 return FormatTypeGUID(p)
1352 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001353 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001354 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001355 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001356 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001357 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001358 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001359 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001360 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001361 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001362 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001363 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001364 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001365 return '[%x]' % (p.Attributes.raw >> 48)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001366 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001367
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001368 def IsFormatArgsSpecified():
1369 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001370
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001371 gpt = GPT.LoadFromFile(args.image_file)
1372 logging.debug('%r', gpt.header)
1373 fmt = '%12s %11s %7s %s'
1374 fmt2 = '%32s %s: %s'
1375 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001376
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001377 if IsFormatArgsSpecified() and args.number is None:
1378 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001379
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001380 if not (args.number is None or
1381 0 < args.number <= gpt.header.PartitionEntriesNumber):
1382 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001383
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001384 partitions = gpt.partitions
1385 do_print_gpt_blocks = False
1386 if not (args.quick or IsFormatArgsSpecified()):
1387 print(fmt % header)
1388 if args.number is None:
1389 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001390
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001391 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001392 if gpt.pmbr:
1393 print(fmt % (0, 1, '', 'PMBR'))
1394 if gpt.is_secondary:
1395 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1396 else:
1397 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1398 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1399 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001400
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001401 for p in partitions:
1402 if args.number is None:
1403 # Skip unused partitions.
1404 if p.IsUnused():
1405 continue
1406 elif p.number != args.number:
1407 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001408
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001409 if IsFormatArgsSpecified():
1410 print(ApplyFormatArgs(p))
1411 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001412
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001413 print(fmt % (p.FirstLBA, p.blocks, p.number,
1414 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001415 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001416
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001417 if not args.quick:
1418 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001419 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1420 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001421 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001422 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001423
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001424 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001425 if gpt.is_secondary:
1426 header = gpt.header
1427 else:
1428 f = args.image_file
1429 f.seek(gpt.header.BackupLBA * gpt.block_size)
1430 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001431 print(fmt % (header.PartitionEntriesStartingLBA,
1432 gpt.GetPartitionTableBlocks(header), '',
1433 'Sec GPT table'))
1434 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001435
Hung-Te Lin3b491672018-04-19 01:41:20 +08001436 # Check integrity after showing all fields.
1437 gpt.CheckIntegrity()
1438
Hung-Te Linfe724f82018-04-18 15:03:58 +08001439 class Prioritize(SubCommand):
1440 """Reorder the priority of all kernel partitions.
1441
1442 Reorder the priority of all active ChromeOS Kernel partitions.
1443
1444 With no options this will set the lowest active kernel to priority 1 while
1445 maintaining the original order.
1446 """
1447
1448 def DefineArgs(self, parser):
1449 parser.add_argument(
1450 '-P', '--priority', type=int,
1451 help=('Highest priority to use in the new ordering. '
1452 'The other partitions will be ranked in decreasing '
1453 'priority while preserving their original order. '
1454 'If necessary the lowest ranks will be coalesced. '
1455 'No active kernels will be lowered to priority 0.'))
1456 parser.add_argument(
1457 '-i', '--number', type=int,
1458 help='Specify the partition to make the highest in the new order.')
1459 parser.add_argument(
1460 '-f', '--friends', action='store_true',
1461 help=('Friends of the given partition (those with the same '
1462 'starting priority) are also updated to the new '
1463 'highest priority. '))
1464 parser.add_argument(
1465 'image_file', type=argparse.FileType('rb+'),
1466 help='Disk image file to prioritize.')
1467
1468 def Execute(self, args):
1469 gpt = GPT.LoadFromFile(args.image_file)
1470 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001471 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1472 groups = dict((k, list(g)) for k, g in itertools.groupby(
1473 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001474 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001475 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001476 if p not in parts:
1477 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001478 pri = p.Attributes.priority
1479 friends = groups.pop(pri)
1480 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001481 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001482 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001483 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001484 groups[new_pri] = [p]
1485 friends.remove(p)
1486 if friends:
1487 groups[pri] = friends
1488
1489 if 0 in groups:
1490 # Do not change any partitions with priority=0
1491 groups.pop(0)
1492
Yilin Yang78fa12e2019-09-25 14:21:10 +08001493 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001494 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001495
1496 # Max priority is 0xf.
1497 highest = min(args.priority or len(prios), 0xf)
1498 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001499
Hung-Te Lin138389f2018-05-15 17:55:00 +08001500 for i, pri in enumerate(prios):
1501 new_priority = max(1, highest - i)
1502 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001503 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001504 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001505 if old_priority == new_priority:
1506 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001507 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001508 if attrs.tries < 1 and not attrs.successful:
1509 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001510 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001511 logging.info('%s priority changed from %s to %s.', p, old_priority,
1512 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001513
1514 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001515 args.image_file.close()
Hung-Te Linfe724f82018-04-18 15:03:58 +08001516
Hung-Te Linf641d302018-04-18 15:09:35 +08001517 class Find(SubCommand):
1518 """Locate a partition by its GUID.
1519
1520 Find a partition by its UUID or label. With no specified DRIVE it scans all
1521 physical drives.
1522
1523 The partition type may also be given as one of these aliases:
1524
1525 firmware ChromeOS firmware
1526 kernel ChromeOS kernel
1527 rootfs ChromeOS rootfs
1528 data Linux data
1529 reserved ChromeOS reserved
1530 efi EFI System Partition
1531 unused Unused (nonexistent) partition
1532 """
1533 def DefineArgs(self, parser):
1534 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001535 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001536 help='Search for Partition Type GUID')
1537 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001538 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001539 help='Search for Partition Unique GUID')
1540 parser.add_argument(
1541 '-l', '--label',
1542 help='Search for Label')
1543 parser.add_argument(
1544 '-n', '--numeric', action='store_true',
1545 help='Numeric output only.')
1546 parser.add_argument(
1547 '-1', '--single-match', action='store_true',
1548 help='Fail if more than one match is found.')
1549 parser.add_argument(
1550 '-M', '--match-file', type=str,
1551 help='Matching partition data must also contain MATCH_FILE content.')
1552 parser.add_argument(
1553 '-O', '--offset', type=int, default=0,
1554 help='Byte offset into partition to match content (default 0).')
1555 parser.add_argument(
1556 'drive', type=argparse.FileType('rb+'), nargs='?',
1557 help='Drive or disk image file to find.')
1558
1559 def Execute(self, args):
1560 if not any((args.type_guid, args.unique_guid, args.label)):
1561 raise GPTError('You must specify at least one of -t, -u, or -l')
1562
1563 drives = [args.drive.name] if args.drive else (
1564 '/dev/%s' % name for name in subprocess.check_output(
Yilin Yang42ba5c62020-05-05 10:32:34 +08001565 'lsblk -d -n -r -o name', shell=True, encoding='utf-8').split())
Hung-Te Linf641d302018-04-18 15:09:35 +08001566
1567 match_pattern = None
1568 if args.match_file:
1569 with open(args.match_file) as f:
1570 match_pattern = f.read()
1571
1572 found = 0
1573 for drive in drives:
1574 try:
1575 gpt = GPT.LoadFromFile(drive)
1576 except GPTError:
1577 if args.drive:
1578 raise
1579 # When scanning all block devices on system, ignore failure.
1580
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001581 def Unmatch(a, b):
1582 return a is not None and a != b
1583
Hung-Te Linf641d302018-04-18 15:09:35 +08001584 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001585 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001586 Unmatch(args.label, p.Names) or
1587 Unmatch(args.unique_guid, p.UniqueGUID) or
1588 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001589 continue
1590 if match_pattern:
1591 with open(drive, 'rb') as f:
1592 f.seek(p.offset + args.offset)
1593 if f.read(len(match_pattern)) != match_pattern:
1594 continue
1595 # Found the partition, now print.
1596 found += 1
1597 if args.numeric:
1598 print(p.number)
1599 else:
1600 # This is actually more for block devices.
1601 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1602 p.number))
1603
1604 if found < 1 or (args.single_match and found > 1):
1605 return 1
1606 return 0
1607
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001608
1609def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001610 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001611 parser = argparse.ArgumentParser(description='GPT Utility.')
1612 parser.add_argument('--verbose', '-v', action='count', default=0,
1613 help='increase verbosity.')
1614 parser.add_argument('--debug', '-d', action='store_true',
1615 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001616 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001617
1618 args = parser.parse_args()
1619 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1620 if args.debug:
1621 log_level = logging.DEBUG
1622 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1623 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001624 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001625 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001626 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001627 sys.exit(code)
Yilin Yang0724c9d2019-11-15 15:53:45 +08001628 elif isinstance(code, str):
Hung-Te Linbad46112018-05-15 16:39:14 +08001629 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001630 except Exception as e:
1631 if args.verbose or args.debug:
1632 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001633 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001634
1635
1636if __name__ == '__main__':
1637 main()