blob: 6e0e0de7ded767c4c58509fc51bc487cc79ee440 [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."""
46 pass
Hung-Te Linc772e1a2017-04-14 16:50:50 +080047
Hung-Te Linc772e1a2017-04-14 16:50:50 +080048
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080049class StructField(object):
50 """Definition of a field in struct.
51
52 Attributes:
53 fmt: a format string for struct.{pack,unpack} to use.
54 name: a string for name of processed field.
55 """
56 __slots__ = ['fmt', 'name']
57
58 def __init__(self, fmt, name):
59 self.fmt = fmt
60 self.name = name
61
62 def Pack(self, value):
63 """"Packs given value from given format."""
64 del self # Unused.
Yilin Yang235e5982019-12-26 10:36:22 +080065 if isinstance(value, str):
66 value = value.encode('utf-8')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080067 return value
68
69 def Unpack(self, value):
70 """Unpacks given value into given format."""
71 del self # Unused.
72 return value
73
74
75class UTF16StructField(StructField):
76 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080077 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080078 encoding = 'utf-16-le'
79
80 def __init__(self, max_length, name):
81 self.max_length = max_length
82 fmt = '%ds' % max_length
83 super(UTF16StructField, self).__init__(fmt, name)
84
85 def Pack(self, value):
86 new_value = value.encode(self.encoding)
87 if len(new_value) >= self.max_length:
88 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
89 (value, self.name, self.max_length))
90 return new_value
91
92 def Unpack(self, value):
93 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080094
Hung-Te Linbf8aa272018-04-19 03:02:29 +080095
96class GUID(uuid.UUID):
97 """A special UUID that defaults to upper case in str()."""
98
99 def __str__(self):
100 """Returns GUID in upper case."""
101 return super(GUID, self).__str__().upper()
102
103 @staticmethod
104 def Random():
105 return uuid.uuid4()
106
107
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800108class GUIDStructField(StructField):
109 """A GUID field."""
110
111 def __init__(self, name):
112 super(GUIDStructField, self).__init__('16s', name)
113
114 def Pack(self, value):
115 if value is None:
Yilin Yang235e5982019-12-26 10:36:22 +0800116 return b'\x00' * 16
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800117 if not isinstance(value, uuid.UUID):
118 raise StructError('Field %s needs a GUID value instead of [%r].' %
119 (self.name, value))
120 return value.bytes_le
121
122 def Unpack(self, value):
123 return GUID(bytes_le=value)
124
125
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800126def BitProperty(getter, setter, shift, mask):
127 """A generator for bit-field properties.
128
129 This is used inside a class to manipulate an integer-like variable using
130 properties. The getter and setter should be member functions to change the
131 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800132
133 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800134 getter: a function to read integer type variable (for all the bits).
135 setter: a function to set the new changed integer type variable.
136 shift: integer for how many bits should be shifted (right).
137 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800138 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800139 def _getter(self):
140 return (getter(self) >> shift) & mask
141 def _setter(self, value):
142 assert value & mask == value, (
143 'Value %s out of range (mask=%s)' % (value, mask))
144 setter(self, getter(self) & ~(mask << shift) | value << shift)
145 return property(_getter, _setter)
146
147
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800148class PartitionAttributes(object):
149 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800150
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800151 This can be created using Partition.attrs, but the changed properties won't
152 apply to underlying Partition until an explicit call with
153 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800154 """
155
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800156 def __init__(self, attrs):
157 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800158
159 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800160 def raw(self):
161 """Returns the raw integer type attributes."""
162 return self._Get()
163
164 def _Get(self):
165 return self._attrs
166
167 def _Set(self, value):
168 self._attrs = value
169
170 successful = BitProperty(_Get, _Set, 56, 1)
171 tries = BitProperty(_Get, _Set, 52, 0xf)
172 priority = BitProperty(_Get, _Set, 48, 0xf)
173 legacy_boot = BitProperty(_Get, _Set, 2, 1)
174 required = BitProperty(_Get, _Set, 0, 1)
175 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
176
177
178class PartitionAttributeStructField(StructField):
179
180 def Pack(self, value):
181 if not isinstance(value, PartitionAttributes):
182 raise StructError('Given value %r is not %s.' %
183 (value, PartitionAttributes.__name__))
184 return value.raw
185
186 def Unpack(self, value):
187 return PartitionAttributes(value)
188
189
Yilin Yang9cf532e2019-12-13 12:02:59 +0800190# The binascii.crc32 returns unsigned integer in python3, so CRC32 in struct
191# must be declared as 'unsigned' (L).
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800192# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
193HEADER_FIELDS = [
194 StructField('8s', 'Signature'),
195 StructField('4s', 'Revision'),
196 StructField('L', 'HeaderSize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800197 StructField('L', 'CRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800198 StructField('4s', 'Reserved'),
199 StructField('Q', 'CurrentLBA'),
200 StructField('Q', 'BackupLBA'),
201 StructField('Q', 'FirstUsableLBA'),
202 StructField('Q', 'LastUsableLBA'),
203 GUIDStructField('DiskGUID'),
204 StructField('Q', 'PartitionEntriesStartingLBA'),
205 StructField('L', 'PartitionEntriesNumber'),
206 StructField('L', 'PartitionEntrySize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800207 StructField('L', 'PartitionArrayCRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800208]
209
210# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
211PARTITION_FIELDS = [
212 GUIDStructField('TypeGUID'),
213 GUIDStructField('UniqueGUID'),
214 StructField('Q', 'FirstLBA'),
215 StructField('Q', 'LastLBA'),
216 PartitionAttributeStructField('Q', 'Attributes'),
217 UTF16StructField(72, 'Names'),
218]
219
220# The PMBR has so many variants. The basic format is defined in
221# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
222# implementation, as derived from `cgpt`, is following syslinux as:
223# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/master/cgpt/cgpt.h#32
224PMBR_FIELDS = [
225 StructField('424s', 'BootCode'),
226 GUIDStructField('BootGUID'),
227 StructField('L', 'DiskID'),
228 StructField('2s', 'Magic'),
229 StructField('16s', 'LegacyPart0'),
230 StructField('16s', 'LegacyPart1'),
231 StructField('16s', 'LegacyPart2'),
232 StructField('16s', 'LegacyPart3'),
233 StructField('2s', 'Signature'),
234]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800235
236
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800237class GPTError(Exception):
238 """All exceptions by GPT."""
239 pass
240
241
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800242class GPTObject(object):
243 """A base object in GUID Partition Table.
244
245 All objects (for instance, header or partition entries) must inherit this
246 class and define the FIELD attribute with a list of field definitions using
247 StructField.
248
249 The 'name' in StructField will become the attribute name of GPT objects that
250 can be directly packed into / unpacked from. Derived (calculated from existing
251 attributes) attributes should be in lower_case.
252
253 It is also possible to attach some additional properties to the object as meta
254 data (for example path of the underlying image file). To do that, first
255 include it in __slots__ list and specify them as dictionary-type args in
256 constructors. These properties will be preserved when you call Clone().
257
258 To create a new object, call the constructor. Field data can be assigned as
259 in arguments, or give nothing to initialize as zero (see Zero()). Field data
260 and meta values can be also specified in keyword arguments (**kargs) at the
261 same time.
262
263 To read a object from file or stream, use class method ReadFrom(source).
264 To make changes, modify the field directly or use Update(dict), or create a
265 copy by Clone() first then Update.
266
267 To wipe all fields (but not meta), call Zero(). There is currently no way
268 to clear meta except setting them to None one by one.
269 """
270 __slots__ = []
271
Peter Shih533566a2018-09-05 17:48:03 +0800272 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800273 """A list of StructField definitions."""
274
275 def __init__(self, *args, **kargs):
276 if args:
277 if len(args) != len(self.FIELDS):
278 raise GPTError('%s need %s arguments (found %s).' %
279 (type(self).__name__, len(self.FIELDS), len(args)))
280 for f, value in zip(self.FIELDS, args):
281 setattr(self, f.name, value)
282 else:
283 self.Zero()
284
285 all_names = [f for f in self.__slots__]
Yilin Yang879fbda2020-05-14 13:52:30 +0800286 for name, value in kargs.items():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800287 if name not in all_names:
288 raise GPTError('%s does not support keyword arg <%s>.' %
289 (type(self).__name__, name))
290 setattr(self, name, value)
291
292 def __iter__(self):
293 """An iterator to return all fields associated in the object."""
294 return (getattr(self, f.name) for f in self.FIELDS)
295
296 def __repr__(self):
297 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800298 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800299
300 @classmethod
301 def GetStructFormat(cls):
302 """Returns a format string for struct to use."""
303 return '<' + ''.join(f.fmt for f in cls.FIELDS)
304
305 @classmethod
306 def ReadFrom(cls, source, **kargs):
307 """Returns an object from given source."""
308 obj = cls(**kargs)
309 obj.Unpack(source)
310 return obj
311
312 @property
313 def blob(self):
314 """The (packed) blob representation of the object."""
315 return self.Pack()
316
317 @property
318 def meta(self):
319 """Meta values (those not in GPT object fields)."""
320 metas = set(self.__slots__) - set([f.name for f in self.FIELDS])
321 return dict((name, getattr(self, name)) for name in metas)
322
323 def Unpack(self, source):
324 """Unpacks values from a given source.
325
326 Args:
327 source: a string of bytes or a file-like object to read from.
328 """
329 fmt = self.GetStructFormat()
330 if source is None:
331 source = '\x00' * struct.calcsize(fmt)
Yilin Yang235e5982019-12-26 10:36:22 +0800332 if not isinstance(source, (str, bytes)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800333 return self.Unpack(source.read(struct.calcsize(fmt)))
Yilin Yang235e5982019-12-26 10:36:22 +0800334 if isinstance(source, str):
335 source = source.encode('utf-8')
336 for f, value in zip(self.FIELDS, struct.unpack(fmt.encode('utf-8'),
337 source)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800338 setattr(self, f.name, f.Unpack(value))
Yilin Yang840fdc42020-01-16 16:37:42 +0800339 return None
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800340
341 def Pack(self):
Yilin Yang235e5982019-12-26 10:36:22 +0800342 """Packs values in all fields into a bytes by struct format."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800343 return struct.pack(self.GetStructFormat(),
344 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
345
346 def Clone(self):
347 """Clones a new instance."""
348 return type(self)(*self, **self.meta)
349
350 def Update(self, **dargs):
351 """Applies multiple values in current object."""
Yilin Yang879fbda2020-05-14 13:52:30 +0800352 for name, value in dargs.items():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800353 setattr(self, name, value)
354
355 def Zero(self):
356 """Set all fields to values representing zero or empty.
357
358 Note the meta attributes won't be cleared.
359 """
360 class ZeroReader(object):
361 """A /dev/zero like stream."""
362
363 @staticmethod
364 def read(num):
365 return '\x00' * num
366
367 self.Unpack(ZeroReader())
368
369
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800370class GPT(object):
371 """A GPT helper class.
372
373 To load GPT from an existing disk image file, use `LoadFromFile`.
374 After modifications were made, use `WriteToFile` to commit changes.
375
376 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800377 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800378 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800379 partitions: a list of GPT partition entry nametuple.
380 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800381 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800382 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800383 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800384 # Old devices uses 'Basic data' type for stateful partition, and newer devices
385 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
386 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800387 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800388 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800389 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
390 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800391 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
392 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
393 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
394 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
395 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800396 }
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800397 TYPE_GUID_FROM_NAME = dict(
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +0800398 ('efi' if v.startswith('EFI') else v.lower().split()[-1], k)
Yilin Yang879fbda2020-05-14 13:52:30 +0800399 for k, v in TYPE_GUID_MAP.items())
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800400 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
401 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
402 TYPE_GUID_LIST_BOOTABLE = [
403 TYPE_GUID_CHROMEOS_KERNEL,
404 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800405 ]
406
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800407 class ProtectiveMBR(GPTObject):
408 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800409 FIELDS = PMBR_FIELDS
410 __slots__ = [f.name for f in FIELDS]
411
Yilin Yang235e5982019-12-26 10:36:22 +0800412 SIGNATURE = b'\x55\xAA'
413 MAGIC = b'\x1d\x9a'
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800414
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800415 class Header(GPTObject):
416 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800417 FIELDS = HEADER_FIELDS
418 __slots__ = [f.name for f in FIELDS]
419
Yilin Yang235e5982019-12-26 10:36:22 +0800420 SIGNATURES = [b'EFI PART', b'CHROMEOS']
421 SIGNATURE_IGNORE = b'IGNOREME'
422 DEFAULT_REVISION = b'\x00\x00\x01\x00'
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800423
424 DEFAULT_PARTITION_ENTRIES = 128
425 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
426
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800427 @classmethod
428 def Create(cls, size, block_size, pad_blocks=0,
429 part_entries=DEFAULT_PARTITION_ENTRIES):
430 """Creates a header with default values.
431
432 Args:
433 size: integer of expected image size.
434 block_size: integer for size of each block (sector).
435 pad_blocks: number of preserved sectors between header and partitions.
436 part_entries: number of partitions to include in header.
437 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800438 PART_FORMAT = GPT.Partition.GetStructFormat()
439 FORMAT = cls.GetStructFormat()
440 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800441 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
442 parts_bytes = part_entries * part_entry_size
Yilin Yang14d02a22019-11-01 11:32:03 +0800443 parts_blocks = parts_bytes // block_size
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800444 if parts_bytes % block_size:
445 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800446 # CRC32 and PartitionsCRC32 must be updated later explicitly.
447 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800448 Signature=cls.SIGNATURES[0],
449 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800450 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800451 CurrentLBA=1,
Yilin Yang14d02a22019-11-01 11:32:03 +0800452 BackupLBA=size // block_size - 1,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800453 FirstUsableLBA=parts_lba + parts_blocks,
Yilin Yang14d02a22019-11-01 11:32:03 +0800454 LastUsableLBA=size // block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800455 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800456 PartitionEntriesStartingLBA=parts_lba,
457 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800458 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800459
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800460 def UpdateChecksum(self):
461 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800462
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800463 Note the PartitionArrayCRC32 is not touched - you have to make sure that
464 is correct before calling Header.UpdateChecksum().
465 """
466 self.Update(CRC32=0)
467 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800468
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800469 class Partition(GPTObject):
470 """The partition entry in GPT.
471
472 Please include following properties when creating a Partition object:
473 - image: a string for path to the image file the partition maps to.
474 - number: the 1-based partition number.
475 - block_size: an integer for size of each block (LBA, or sector).
476 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800477 FIELDS = PARTITION_FIELDS
478 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800479 NAMES_ENCODING = 'utf-16-le'
480 NAMES_LENGTH = 72
481
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800482 def __str__(self):
483 return '%s#%s' % (self.image, self.number)
484
485 def IsUnused(self):
486 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800487 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800488
Hung-Te Linfe724f82018-04-18 15:03:58 +0800489 def IsChromeOSKernel(self):
490 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800491 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800492
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800493 @property
494 def blocks(self):
495 """Return size of partition in blocks (see block_size)."""
496 return self.LastLBA - self.FirstLBA + 1
497
498 @property
499 def offset(self):
500 """Returns offset to partition in bytes."""
501 return self.FirstLBA * self.block_size
502
503 @property
504 def size(self):
505 """Returns size of partition in bytes."""
506 return self.blocks * self.block_size
507
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800508 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800509 """GPT constructor.
510
511 See LoadFromFile for how it's usually used.
512 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800513 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800514 self.header = None
515 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800516 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800517 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800518
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800519 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800520 def GetTypeGUID(cls, value):
521 """The value may be a GUID in string or a short type string."""
522 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
523 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800524
525 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800526 def Create(cls, image_name, size, block_size, pad_blocks=0):
527 """Creates a new GPT instance from given size and block_size.
528
529 Args:
530 image_name: a string of underlying disk image file name.
531 size: expected size of disk image.
532 block_size: size of each block (sector) in bytes.
533 pad_blocks: number of blocks between header and partitions array.
534 """
535 gpt = cls()
536 gpt.block_size = block_size
537 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
538 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800539 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800540 for i in range(gpt.header.PartitionEntriesNumber)]
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800541 return gpt
542
Hung-Te Lin446eb512018-05-02 18:39:16 +0800543 @staticmethod
544 def IsBlockDevice(image):
545 """Returns if the image is a block device file."""
546 return stat.S_ISBLK(os.stat(image).st_mode)
547
548 @classmethod
549 def GetImageSize(cls, image):
550 """Returns the size of specified image (plain or block device file)."""
551 if not cls.IsBlockDevice(image):
552 return os.path.getsize(image)
553
554 fd = os.open(image, os.O_RDONLY)
555 try:
556 return os.lseek(fd, 0, os.SEEK_END)
557 finally:
558 os.close(fd)
559
560 @classmethod
561 def GetLogicalBlockSize(cls, block_dev):
562 """Returns the logical block (sector) size from a block device file.
563
564 The underlying call is BLKSSZGET. An alternative command is blockdev,
565 but that needs root permission even if we just want to get sector size.
566 """
567 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
568 return int(subprocess.check_output(
569 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
570
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800571 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800572 def LoadFromFile(cls, image):
573 """Loads a GPT table from give disk image file object.
574
575 Args:
576 image: a string as file path or a file-like object to read from.
577 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800578 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800579 with open(image, 'rb') as f:
580 return cls.LoadFromFile(f)
581
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800582 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800583 image.seek(0)
584 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
585 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
586 logging.debug('Found MBR signature in %s', image.name)
587 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
588 logging.debug('Found PMBR in %s', image.name)
589 gpt.pmbr = pmbr
590
Hung-Te Linf148d322018-04-13 10:24:42 +0800591 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800592 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
593 if cls.IsBlockDevice(image.name):
594 block_sizes = [cls.GetLogicalBlockSize(image.name)]
595
596 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800597 # Note because there are devices setting Primary as ignored and the
598 # partition table signature accepts 'CHROMEOS' which is also used by
599 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
600 # first before trying other block sizes, otherwise we may incorrectly
601 # identify a kernel partition as LBA 1 of larger block size system.
602 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
603 (-block_size, os.SEEK_END)]):
604 image.seek(*seek)
605 header = gpt.Header.ReadFrom(image)
606 if header.Signature in cls.Header.SIGNATURES:
607 gpt.block_size = block_size
608 if i != 0:
609 gpt.is_secondary = True
610 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800611 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800612 else:
613 # Nothing found, try next block size.
614 continue
615 # Found a valid signature.
616 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800617 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800618 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800619
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800620 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800621 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800622 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800623 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800624 return p
625
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800626 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800627 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800628 ReadPartition(image, i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800629 for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800630 return gpt
631
Hung-Te Linc5196682018-04-18 22:59:59 +0800632 def GetUsedPartitions(self):
633 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800634
Hung-Te Linc5196682018-04-18 22:59:59 +0800635 Use 'number' property to find the real location of partition in
636 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800637 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800638 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800639
640 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800641 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800642 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800643 return (max(p.LastLBA for p in parts)
644 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800645
646 def GetPartitionTableBlocks(self, header=None):
647 """Returns the blocks (or LBA) of partition table from given header."""
648 if header is None:
649 header = self.header
650 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Yilin Yang14d02a22019-11-01 11:32:03 +0800651 blocks = size // self.block_size
Hung-Te Linf148d322018-04-13 10:24:42 +0800652 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800653 blocks += 1
654 return blocks
655
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800656 def GetPartition(self, number):
657 """Gets the Partition by given (1-based) partition number.
658
659 Args:
660 number: an integer as 1-based partition number.
661 """
662 if not 0 < number <= len(self.partitions):
663 raise GPTError('Invalid partition number %s.' % number)
664 return self.partitions[number - 1]
665
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800666 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800667 """Updates the entry in partition table by given Partition object.
668
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800669 Usually you only need to call this if you want to copy one partition to
670 different location (number of image).
671
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800672 Args:
673 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800674 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800675 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800676 ref = self.partitions[number - 1]
677 part = part.Clone()
678 part.number = number
679 part.image = ref.image
680 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800681 self.partitions[number - 1] = part
682
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800683 def GetSize(self):
684 return self.block_size * (self.header.BackupLBA + 1)
685
686 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800687 """Adjust GPT for a disk image in given size.
688
689 Args:
690 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800691 check_overlap: Checks if the backup partition table overlaps used
692 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800693 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800694 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800695 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800696 raise GPTError(
697 'New file size %d is not valid for image files.' % new_size)
Yilin Yang14d02a22019-11-01 11:32:03 +0800698 new_blocks = new_size // self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800699 if old_size != new_size:
Yilin Yang9881b1e2019-12-11 11:47:33 +0800700 logging.warning('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
701 new_size, new_blocks, old_size,
702 old_size // self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800703 else:
704 logging.info('Image size (%d, LBA=%d) not changed.',
705 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800706 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800707
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800708 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800709 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800710 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800711
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800712 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800713 max_used_lba = self.GetMaxUsedLBA()
714 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800715 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800716
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800717 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800718
719 def GetFreeSpace(self):
720 """Returns the free (available) space left according to LastUsableLBA."""
721 max_lba = self.GetMaxUsedLBA()
722 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800723 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800724
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800725 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800726 """Expands a given partition to last usable LBA.
727
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800728 The size of the partition can actually be reduced if the last usable LBA
729 decreases.
730
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800731 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800732 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800733
734 Returns:
735 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800736 """
737 # Assume no partitions overlap, we need to make sure partition[i] has
738 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800739 p = self.GetPartition(number)
740 if p.IsUnused():
741 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800742 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800743 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800744 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800745 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800746 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800747
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800748 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800749 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800750 new_blocks = p.blocks
Yilin Yang9881b1e2019-12-11 11:47:33 +0800751 logging.warning(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800752 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800753 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800754
Hung-Te Lin3b491672018-04-19 01:41:20 +0800755 def CheckIntegrity(self):
756 """Checks if the GPT objects all look good."""
757 # Check if the header allocation looks good. CurrentLBA and
758 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
759 # LastUsableLBA].
760 header = self.header
761 entries_first_lba = header.PartitionEntriesStartingLBA
762 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
763
764 def CheckOutsideUsable(name, lba, outside_entries=False):
765 if lba < 1:
766 raise GPTError('%s should not live in LBA %s.' % (name, lba))
767 if lba > max(header.BackupLBA, header.CurrentLBA):
768 # Note this is "in theory" possible, but we want to report this as
769 # error as well, since it usually leads to error.
770 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
771 (name, lba, header.BackupLBA))
772 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
773 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
774 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
775 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
776 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
777 (name, lba, entries_first_lba, entries_last_lba))
778 CheckOutsideUsable('Header', header.CurrentLBA, True)
779 CheckOutsideUsable('Backup header', header.BackupLBA, True)
780 CheckOutsideUsable('Partition entries', entries_first_lba)
781 CheckOutsideUsable('Partition entries end', entries_last_lba)
782
783 parts = self.GetUsedPartitions()
784 # Check if partition entries overlap with each other.
785 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
786 lba_list.sort(key=lambda t: t[0])
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800787 for i in range(len(lba_list) - 1):
Hung-Te Lin3b491672018-04-19 01:41:20 +0800788 if lba_list[i][1] >= lba_list[i + 1][0]:
789 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
790 (lba_list[i] + lba_list[i + 1]))
791 # Now, check the first and last partition.
792 if lba_list:
793 p = lba_list[0][2]
794 if p.FirstLBA < header.FirstUsableLBA:
795 raise GPTError(
796 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
797 (p, p.FirstLBA, header.FirstLBA))
798 p = lba_list[-1][2]
799 if p.LastLBA > header.LastUsableLBA:
800 raise GPTError(
801 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
802 (p, p.LastLBA, header.LastLBA))
803 # Check if UniqueGUIDs are not unique.
804 if len(set(p.UniqueGUID for p in parts)) != len(parts):
805 raise GPTError('Partition UniqueGUIDs are duplicated.')
806 # Check if CRCs match.
Yilin Yang235e5982019-12-26 10:36:22 +0800807 if (binascii.crc32(b''.join(p.blob for p in self.partitions)) !=
Hung-Te Lin3b491672018-04-19 01:41:20 +0800808 header.PartitionArrayCRC32):
809 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800810 header_crc = header.Clone()
811 header_crc.UpdateChecksum()
812 if header_crc.CRC32 != header.CRC32:
813 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800814
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800815 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800816 """Updates all checksum fields in GPT objects."""
Yilin Yang235e5982019-12-26 10:36:22 +0800817 parts = b''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800818 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
819 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800820
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800821 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800822 """Returns the backup header according to given header.
823
824 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
825 fields.
826 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800827 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800828 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800829 h = header.Clone()
830 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800831 BackupLBA=header.CurrentLBA,
832 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800833 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800834 h.UpdateChecksum()
835 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800836
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800837 @classmethod
838 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
839 """Writes a protective MBR to given file.
840
841 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
842 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
843 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
844 we can follow that.
845
846 Args:
847 create: True to re-create PMBR structure.
848 bootcode: a blob of new boot code.
849 boot_guid a blob for new boot GUID.
850
851 Returns:
852 The written PMBR structure.
853 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800854 if isinstance(image, str):
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800855 with open(image, 'rb+') as f:
856 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
857
858 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800859 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
860 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800861 pmbr = cls.ProtectiveMBR.ReadFrom(image)
862
863 if create:
864 legacy_sectors = min(
865 0x100000000,
Yilin Yang14d02a22019-11-01 11:32:03 +0800866 GPT.GetImageSize(image.name) // cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800867 # Partition 0 must have have the fixed CHS with number of sectors
868 # (calculated as legacy_sectors later).
Yilin Yangf9fe1932019-11-04 17:09:34 +0800869 part0 = (codecs.decode('00000200eeffffff01000000', 'hex') +
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800870 struct.pack('<I', legacy_sectors))
871 # Partition 1~3 should be all zero.
872 part1 = '\x00' * 16
873 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800874 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800875 BootGUID=cls.TYPE_GUID_UNUSED,
876 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800877 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800878 LegacyPart0=part0,
879 LegacyPart1=part1,
880 LegacyPart2=part1,
881 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800882 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800883
884 if bootcode:
885 if len(bootcode) > len(pmbr.BootCode):
886 logging.info(
887 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
888 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800889 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800890 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800891 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800892
893 blob = pmbr.blob
894 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
895 image.seek(0)
896 image.write(blob)
897 return pmbr
898
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800899 def WriteToFile(self, image):
900 """Updates partition table in a disk image file.
901
902 Args:
903 image: a string as file path or a file-like object to write into.
904 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800905 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800906 with open(image, 'rb+') as f:
907 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800908
909 def WriteData(name, blob, lba):
910 """Writes a blob into given location."""
911 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800912 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800913 image.seek(lba * self.block_size)
914 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800915
916 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800917 self.CheckIntegrity()
Yilin Yang235e5982019-12-26 10:36:22 +0800918 parts_blob = b''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800919
920 header = self.header
921 WriteData('GPT Header', header.blob, header.CurrentLBA)
922 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
923 logging.info(
924 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
925 header.LastUsableLBA)
926
927 if not self.is_secondary:
928 # When is_secondary is True, the header we have is actually backup header.
929 backup_header = self.GetBackupHeader(self.header)
930 WriteData(
931 'Backup Partitions', parts_blob,
932 backup_header.PartitionEntriesStartingLBA)
933 WriteData(
934 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Yilin Yang840fdc42020-01-16 16:37:42 +0800935 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800936
937
938class GPTCommands(object):
939 """Collection of GPT sub commands for command line to use.
940
941 The commands are derived from `cgpt`, but not necessary to be 100% compatible
942 with cgpt.
943 """
944
945 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800946 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800947 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800948 ('type', 'type guid'),
949 ('unique', 'unique guid'),
950 ('label', 'label'),
951 ('Successful', 'Successful flag'),
952 ('Tries', 'Tries flag'),
953 ('Priority', 'Priority flag'),
954 ('Legacy', 'Legacy Boot flag'),
955 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800956
957 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800958 commands = dict(
959 (command.lower(), getattr(self, command)())
960 for command in dir(self)
961 if (isinstance(getattr(self, command), type) and
962 issubclass(getattr(self, command), self.SubCommand) and
963 getattr(self, command) is not self.SubCommand)
964 )
965 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800966
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800967 def DefineArgs(self, parser):
968 """Defines all available commands to an argparser subparsers instance."""
969 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
Yilin Yang879fbda2020-05-14 13:52:30 +0800970 for name, instance in sorted(self.commands.items()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800971 parser = subparsers.add_parser(
972 name, description=instance.__doc__,
973 formatter_class=argparse.RawDescriptionHelpFormatter,
974 help=instance.__doc__.splitlines()[0])
975 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800976
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800977 def Execute(self, args):
978 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800979 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800980
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800981 class SubCommand(object):
982 """A base class for sub commands to derive from."""
983
984 def DefineArgs(self, parser):
985 """Defines command line arguments to argparse parser.
986
987 Args:
988 parser: An argparse parser instance.
989 """
990 del parser # Unused.
991 raise NotImplementedError
992
993 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800994 """Execute the command with parsed arguments.
995
996 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800997
998 Args:
999 args: An argparse parsed namespace.
1000 """
1001 del args # Unused.
1002 raise NotImplementedError
1003
Hung-Te Line0d1fa72018-05-15 00:04:48 +08001004 def ExecuteCommandLine(self, *args):
1005 """Execute as invoked from command line.
1006
1007 This provides an easy way to execute particular sub command without
1008 creating argument parser explicitly.
1009
1010 Args:
1011 args: a list of string type command line arguments.
1012 """
1013 parser = argparse.ArgumentParser()
1014 self.DefineArgs(parser)
1015 return self.Execute(parser.parse_args(args))
1016
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001017 class Create(SubCommand):
1018 """Create or reset GPT headers and tables.
1019
1020 Create or reset an empty GPT.
1021 """
1022
1023 def DefineArgs(self, parser):
1024 parser.add_argument(
1025 '-z', '--zero', action='store_true',
1026 help='Zero the sectors of the GPT table and entries')
1027 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001028 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001029 help=('Size (in blocks) of the disk to pad between the '
1030 'primary GPT header and its entries, default %(default)s'))
1031 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001032 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001033 help='Size of each block (sector) in bytes.')
1034 parser.add_argument(
1035 'image_file', type=argparse.FileType('rb+'),
1036 help='Disk image file to create.')
1037
1038 def Execute(self, args):
1039 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001040 if block_size is None:
1041 if GPT.IsBlockDevice(args.image_file.name):
1042 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1043 else:
1044 block_size = GPT.DEFAULT_BLOCK_SIZE
1045
1046 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1047 logging.info('Block (sector) size for %s is set to %s bytes.',
1048 args.image_file.name, block_size)
1049
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001050 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001051 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001052 block_size, args.pad_blocks)
1053 if args.zero:
1054 # In theory we only need to clear LBA 1, but to make sure images already
1055 # initialized with different block size won't have GPT signature in
1056 # different locations, we should zero until first usable LBA.
1057 args.image_file.seek(0)
Yilin Yang235e5982019-12-26 10:36:22 +08001058 args.image_file.write(b'\0' * block_size * gpt.header.FirstUsableLBA)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001059 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001060 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001061 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001062
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001063 class Boot(SubCommand):
1064 """Edit the PMBR sector for legacy BIOSes.
1065
1066 With no options, it will just print the PMBR boot guid.
1067 """
1068
1069 def DefineArgs(self, parser):
1070 parser.add_argument(
1071 '-i', '--number', type=int,
1072 help='Set bootable partition')
1073 parser.add_argument(
Stimim Chen0e6071b2020-04-28 18:08:49 +08001074 '-b', '--bootloader', type=argparse.FileType('rb'),
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001075 help='Install bootloader code in the PMBR')
1076 parser.add_argument(
1077 '-p', '--pmbr', action='store_true',
1078 help='Create legacy PMBR partition table')
1079 parser.add_argument(
1080 'image_file', type=argparse.FileType('rb+'),
1081 help='Disk image file to change PMBR.')
1082
1083 def Execute(self, args):
1084 """Rebuilds the protective MBR."""
1085 bootcode = args.bootloader.read() if args.bootloader else None
1086 boot_guid = None
1087 if args.number is not None:
1088 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001089 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001090 pmbr = GPT.WriteProtectiveMBR(
1091 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1092
You-Cheng Syufff7f422018-05-14 15:37:39 +08001093 print(pmbr.BootGUID)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001094 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001095 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001096
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001097 class Legacy(SubCommand):
1098 """Switch between GPT and Legacy GPT.
1099
1100 Switch GPT header signature to "CHROMEOS".
1101 """
1102
1103 def DefineArgs(self, parser):
1104 parser.add_argument(
1105 '-e', '--efi', action='store_true',
1106 help='Switch GPT header signature back to "EFI PART"')
1107 parser.add_argument(
1108 '-p', '--primary-ignore', action='store_true',
1109 help='Switch primary GPT header signature to "IGNOREME"')
1110 parser.add_argument(
1111 'image_file', type=argparse.FileType('rb+'),
1112 help='Disk image file to change.')
1113
1114 def Execute(self, args):
1115 gpt = GPT.LoadFromFile(args.image_file)
1116 # cgpt behavior: if -p is specified, -e is ignored.
1117 if args.primary_ignore:
1118 if gpt.is_secondary:
1119 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1120 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1121 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1122 gpt.header = gpt.GetBackupHeader(self.header)
1123 gpt.is_secondary = True
1124 else:
1125 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001126 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001127 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001128 args.image_file.close()
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001129 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001130 return ('Set %s primary GPT header to %s.' %
1131 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001132 return ('Changed GPT signature for %s to %s.' %
1133 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001134
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001135 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001136 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001137
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001138 def DefineArgs(self, parser):
1139 parser.add_argument(
1140 'image_file', type=argparse.FileType('rb+'),
1141 help='Disk image file to repair.')
1142
1143 def Execute(self, args):
1144 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001145 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001146 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001147 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001148 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001149
1150 class Expand(SubCommand):
1151 """Expands a GPT partition to all available free space."""
1152
1153 def DefineArgs(self, parser):
1154 parser.add_argument(
1155 '-i', '--number', type=int, required=True,
1156 help='The partition to expand.')
1157 parser.add_argument(
1158 'image_file', type=argparse.FileType('rb+'),
1159 help='Disk image file to modify.')
1160
1161 def Execute(self, args):
1162 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001163 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001164 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001165 args.image_file.close()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001166 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001167 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001168 'Partition %s on disk image file %s has been extended '
1169 'from %s to %s .' %
1170 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1171 new_blocks * gpt.block_size))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001172 return ('Nothing to expand for disk image %s partition %s.' %
1173 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001174
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001175 class Add(SubCommand):
1176 """Add, edit, or remove a partition entry.
1177
1178 Use the -i option to modify an existing partition.
1179 The -b, -s, and -t options must be given for new partitions.
1180
1181 The partition type may also be given as one of these aliases:
1182
1183 firmware ChromeOS firmware
1184 kernel ChromeOS kernel
1185 rootfs ChromeOS rootfs
1186 data Linux data
1187 reserved ChromeOS reserved
1188 efi EFI System Partition
1189 unused Unused (nonexistent) partition
1190 """
1191 def DefineArgs(self, parser):
1192 parser.add_argument(
1193 '-i', '--number', type=int,
1194 help='Specify partition (default is next available)')
1195 parser.add_argument(
1196 '-b', '--begin', type=int,
1197 help='Beginning sector')
1198 parser.add_argument(
1199 '-s', '--sectors', type=int,
1200 help='Size in sectors (logical blocks).')
1201 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001202 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001203 help='Partition Type GUID')
1204 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001205 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001206 help='Partition Unique ID')
1207 parser.add_argument(
1208 '-l', '--label',
1209 help='Label')
1210 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001211 '-S', '--successful', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001212 help='set Successful flag')
1213 parser.add_argument(
1214 '-T', '--tries', type=int,
1215 help='set Tries flag (0-15)')
1216 parser.add_argument(
1217 '-P', '--priority', type=int,
1218 help='set Priority flag (0-15)')
1219 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001220 '-R', '--required', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001221 help='set Required flag')
1222 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001223 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001224 choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001225 help='set Legacy Boot flag')
1226 parser.add_argument(
1227 '-A', '--attribute', dest='raw_16', type=int,
1228 help='set raw 16-bit attribute value (bits 48-63)')
1229 parser.add_argument(
1230 'image_file', type=argparse.FileType('rb+'),
1231 help='Disk image file to modify.')
1232
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001233 def Execute(self, args):
1234 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001235 number = args.number
1236 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001237 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001238
1239 # First and last LBA must be calculated explicitly because the given
1240 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001241 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001242 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001243
1244 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001245 part.Zero()
1246 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001247 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001248 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001249 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001250 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001251
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001252 def UpdateAttr(name):
1253 value = getattr(args, name)
1254 if value is None:
1255 return
1256 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001257
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001258 def GetArg(arg_value, default_value):
1259 return default_value if arg_value is None else arg_value
1260
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001261 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001262 for name in ['legacy_boot', 'required', 'priority', 'tries',
1263 'successful', 'raw_16']:
1264 UpdateAttr(name)
1265 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001266 part.Update(
1267 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001268 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001269 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1270 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1271 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1272 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001273
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001274 # Wipe partition again if it should be empty.
1275 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001276 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001277
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001278 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001279 args.image_file.close()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001280 if part.IsUnused():
1281 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001282 return 'Deleted (zeroed) %s.' % (part,)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001283 return ('%s %s (%s+%s).' %
1284 ('Added' if is_new_part else 'Modified',
1285 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001286
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001287 class Show(SubCommand):
1288 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001289
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001290 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001291 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001292
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001293 def DefineArgs(self, parser):
1294 parser.add_argument(
1295 '--numeric', '-n', action='store_true',
1296 help='Numeric output only.')
1297 parser.add_argument(
1298 '--quick', '-q', action='store_true',
1299 help='Quick output.')
1300 parser.add_argument(
1301 '-i', '--number', type=int,
1302 help='Show specified partition only, with format args.')
1303 for name, help_str in GPTCommands.FORMAT_ARGS:
1304 # TODO(hungte) Alert if multiple args were specified.
1305 parser.add_argument(
1306 '--%s' % name, '-%c' % name[0], action='store_true',
1307 help='[format] %s.' % help_str)
1308 parser.add_argument(
1309 'image_file', type=argparse.FileType('rb'),
1310 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001311
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001312 def Execute(self, args):
1313 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001314
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001315 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001316 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001317 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001318 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001319 if names:
1320 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001321 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001322
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001323 def IsBootableType(guid):
1324 if not guid:
1325 return False
1326 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001327
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001328 def FormatAttribute(attrs, chromeos_kernel=False):
1329 if args.numeric:
1330 return '[%x]' % (attrs.raw >> 48)
1331 results = []
1332 if chromeos_kernel:
1333 results += [
1334 'priority=%d' % attrs.priority,
1335 'tries=%d' % attrs.tries,
1336 'successful=%d' % attrs.successful]
1337 if attrs.required:
1338 results += ['required=1']
1339 if attrs.legacy_boot:
1340 results += ['legacy_boot=1']
1341 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001342
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001343 def ApplyFormatArgs(p):
1344 if args.begin:
1345 return p.FirstLBA
1346 elif args.size:
1347 return p.blocks
1348 elif args.type:
1349 return FormatTypeGUID(p)
1350 elif args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001351 return p.UniqueGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001352 elif args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001353 return p.Names
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001354 elif args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001355 return p.Attributes.successful
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001356 elif args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001357 return p.Attributes.priority
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001358 elif args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001359 return p.Attributes.tries
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001360 elif args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001361 return p.Attributes.legacy_boot
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001362 elif args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001363 return '[%x]' % (p.Attributes.raw >> 48)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001364 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001365
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001366 def IsFormatArgsSpecified():
1367 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001368
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001369 gpt = GPT.LoadFromFile(args.image_file)
1370 logging.debug('%r', gpt.header)
1371 fmt = '%12s %11s %7s %s'
1372 fmt2 = '%32s %s: %s'
1373 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001374
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001375 if IsFormatArgsSpecified() and args.number is None:
1376 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001377
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001378 if not (args.number is None or
1379 0 < args.number <= gpt.header.PartitionEntriesNumber):
1380 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001381
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001382 partitions = gpt.partitions
1383 do_print_gpt_blocks = False
1384 if not (args.quick or IsFormatArgsSpecified()):
1385 print(fmt % header)
1386 if args.number is None:
1387 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001388
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001389 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001390 if gpt.pmbr:
1391 print(fmt % (0, 1, '', 'PMBR'))
1392 if gpt.is_secondary:
1393 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1394 else:
1395 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1396 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1397 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001398
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001399 for p in partitions:
1400 if args.number is None:
1401 # Skip unused partitions.
1402 if p.IsUnused():
1403 continue
1404 elif p.number != args.number:
1405 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001406
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001407 if IsFormatArgsSpecified():
1408 print(ApplyFormatArgs(p))
1409 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001410
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001411 print(fmt % (p.FirstLBA, p.blocks, p.number,
1412 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001413 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001414
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001415 if not args.quick:
1416 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001417 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1418 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001419 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001420 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001421
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001422 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001423 if gpt.is_secondary:
1424 header = gpt.header
1425 else:
1426 f = args.image_file
1427 f.seek(gpt.header.BackupLBA * gpt.block_size)
1428 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001429 print(fmt % (header.PartitionEntriesStartingLBA,
1430 gpt.GetPartitionTableBlocks(header), '',
1431 'Sec GPT table'))
1432 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001433
Hung-Te Lin3b491672018-04-19 01:41:20 +08001434 # Check integrity after showing all fields.
1435 gpt.CheckIntegrity()
1436
Hung-Te Linfe724f82018-04-18 15:03:58 +08001437 class Prioritize(SubCommand):
1438 """Reorder the priority of all kernel partitions.
1439
1440 Reorder the priority of all active ChromeOS Kernel partitions.
1441
1442 With no options this will set the lowest active kernel to priority 1 while
1443 maintaining the original order.
1444 """
1445
1446 def DefineArgs(self, parser):
1447 parser.add_argument(
1448 '-P', '--priority', type=int,
1449 help=('Highest priority to use in the new ordering. '
1450 'The other partitions will be ranked in decreasing '
1451 'priority while preserving their original order. '
1452 'If necessary the lowest ranks will be coalesced. '
1453 'No active kernels will be lowered to priority 0.'))
1454 parser.add_argument(
1455 '-i', '--number', type=int,
1456 help='Specify the partition to make the highest in the new order.')
1457 parser.add_argument(
1458 '-f', '--friends', action='store_true',
1459 help=('Friends of the given partition (those with the same '
1460 'starting priority) are also updated to the new '
1461 'highest priority. '))
1462 parser.add_argument(
1463 'image_file', type=argparse.FileType('rb+'),
1464 help='Disk image file to prioritize.')
1465
1466 def Execute(self, args):
1467 gpt = GPT.LoadFromFile(args.image_file)
1468 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001469 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
1470 groups = dict((k, list(g)) for k, g in itertools.groupby(
1471 parts, lambda p: p.Attributes.priority))
Hung-Te Linfe724f82018-04-18 15:03:58 +08001472 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001473 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001474 if p not in parts:
1475 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001476 pri = p.Attributes.priority
1477 friends = groups.pop(pri)
1478 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001479 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001480 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001481 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001482 groups[new_pri] = [p]
1483 friends.remove(p)
1484 if friends:
1485 groups[pri] = friends
1486
1487 if 0 in groups:
1488 # Do not change any partitions with priority=0
1489 groups.pop(0)
1490
Yilin Yang78fa12e2019-09-25 14:21:10 +08001491 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001492 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001493
1494 # Max priority is 0xf.
1495 highest = min(args.priority or len(prios), 0xf)
1496 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001497
Hung-Te Lin138389f2018-05-15 17:55:00 +08001498 for i, pri in enumerate(prios):
1499 new_priority = max(1, highest - i)
1500 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001501 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001502 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001503 if old_priority == new_priority:
1504 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001505 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001506 if attrs.tries < 1 and not attrs.successful:
1507 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001508 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001509 logging.info('%s priority changed from %s to %s.', p, old_priority,
1510 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001511
1512 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001513 args.image_file.close()
Hung-Te Linfe724f82018-04-18 15:03:58 +08001514
Hung-Te Linf641d302018-04-18 15:09:35 +08001515 class Find(SubCommand):
1516 """Locate a partition by its GUID.
1517
1518 Find a partition by its UUID or label. With no specified DRIVE it scans all
1519 physical drives.
1520
1521 The partition type may also be given as one of these aliases:
1522
1523 firmware ChromeOS firmware
1524 kernel ChromeOS kernel
1525 rootfs ChromeOS rootfs
1526 data Linux data
1527 reserved ChromeOS reserved
1528 efi EFI System Partition
1529 unused Unused (nonexistent) partition
1530 """
1531 def DefineArgs(self, parser):
1532 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001533 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001534 help='Search for Partition Type GUID')
1535 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001536 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001537 help='Search for Partition Unique GUID')
1538 parser.add_argument(
1539 '-l', '--label',
1540 help='Search for Label')
1541 parser.add_argument(
1542 '-n', '--numeric', action='store_true',
1543 help='Numeric output only.')
1544 parser.add_argument(
1545 '-1', '--single-match', action='store_true',
1546 help='Fail if more than one match is found.')
1547 parser.add_argument(
1548 '-M', '--match-file', type=str,
1549 help='Matching partition data must also contain MATCH_FILE content.')
1550 parser.add_argument(
1551 '-O', '--offset', type=int, default=0,
1552 help='Byte offset into partition to match content (default 0).')
1553 parser.add_argument(
1554 'drive', type=argparse.FileType('rb+'), nargs='?',
1555 help='Drive or disk image file to find.')
1556
1557 def Execute(self, args):
1558 if not any((args.type_guid, args.unique_guid, args.label)):
1559 raise GPTError('You must specify at least one of -t, -u, or -l')
1560
1561 drives = [args.drive.name] if args.drive else (
1562 '/dev/%s' % name for name in subprocess.check_output(
Yilin Yang42ba5c62020-05-05 10:32:34 +08001563 'lsblk -d -n -r -o name', shell=True, encoding='utf-8').split())
Hung-Te Linf641d302018-04-18 15:09:35 +08001564
1565 match_pattern = None
1566 if args.match_file:
1567 with open(args.match_file) as f:
1568 match_pattern = f.read()
1569
1570 found = 0
1571 for drive in drives:
1572 try:
1573 gpt = GPT.LoadFromFile(drive)
1574 except GPTError:
1575 if args.drive:
1576 raise
1577 # When scanning all block devices on system, ignore failure.
1578
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001579 def Unmatch(a, b):
1580 return a is not None and a != b
1581
Hung-Te Linf641d302018-04-18 15:09:35 +08001582 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001583 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001584 Unmatch(args.label, p.Names) or
1585 Unmatch(args.unique_guid, p.UniqueGUID) or
1586 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001587 continue
1588 if match_pattern:
1589 with open(drive, 'rb') as f:
1590 f.seek(p.offset + args.offset)
1591 if f.read(len(match_pattern)) != match_pattern:
1592 continue
1593 # Found the partition, now print.
1594 found += 1
1595 if args.numeric:
1596 print(p.number)
1597 else:
1598 # This is actually more for block devices.
1599 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1600 p.number))
1601
1602 if found < 1 or (args.single_match and found > 1):
1603 return 1
1604 return 0
1605
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001606
1607def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001608 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001609 parser = argparse.ArgumentParser(description='GPT Utility.')
1610 parser.add_argument('--verbose', '-v', action='count', default=0,
1611 help='increase verbosity.')
1612 parser.add_argument('--debug', '-d', action='store_true',
1613 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001614 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001615
1616 args = parser.parse_args()
1617 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1618 if args.debug:
1619 log_level = logging.DEBUG
1620 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1621 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001622 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001623 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001624 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001625 sys.exit(code)
Yilin Yang0724c9d2019-11-15 15:53:45 +08001626 elif isinstance(code, str):
Hung-Te Linbad46112018-05-15 16:39:14 +08001627 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001628 except Exception as e:
1629 if args.verbose or args.debug:
1630 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001631 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001632
1633
1634if __name__ == '__main__':
1635 main()