blob: 89ecada44bf69567257398c52e4b9e2e1f34c2da [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
Hung-Te Linc772e1a2017-04-14 16:50:50 +080028import argparse
29import binascii
Yilin Yangf9fe1932019-11-04 17:09:34 +080030import codecs
Hung-Te Lin138389f2018-05-15 17:55:00 +080031import itertools
Hung-Te Linc772e1a2017-04-14 16:50:50 +080032import logging
33import os
Hung-Te Lin446eb512018-05-02 18:39:16 +080034import stat
Hung-Te Linc772e1a2017-04-14 16:50:50 +080035import struct
Hung-Te Linf641d302018-04-18 15:09:35 +080036import subprocess
37import sys
Hung-Te Linc772e1a2017-04-14 16:50:50 +080038import uuid
39
40
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080041class StructError(Exception):
42 """Exceptions in packing and unpacking from/to struct fields."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +080043
Hung-Te Linc772e1a2017-04-14 16:50:50 +080044
Fei Shaobd07c9a2020-06-15 19:04:50 +080045class StructField:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080046 """Definition of a field in struct.
47
48 Attributes:
49 fmt: a format string for struct.{pack,unpack} to use.
50 name: a string for name of processed field.
51 """
52 __slots__ = ['fmt', 'name']
53
54 def __init__(self, fmt, name):
55 self.fmt = fmt
56 self.name = name
57
58 def Pack(self, value):
59 """"Packs given value from given format."""
60 del self # Unused.
Yilin Yang235e5982019-12-26 10:36:22 +080061 if isinstance(value, str):
62 value = value.encode('utf-8')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080063 return value
64
65 def Unpack(self, value):
66 """Unpacks given value into given format."""
67 del self # Unused.
68 return value
69
70
71class UTF16StructField(StructField):
72 """A field in UTF encoded string."""
Yilin Yange4e40e92019-10-31 09:57:57 +080073 __slots__ = ['max_length']
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +080074 encoding = 'utf-16-le'
75
76 def __init__(self, max_length, name):
77 self.max_length = max_length
78 fmt = '%ds' % max_length
79 super(UTF16StructField, self).__init__(fmt, name)
80
81 def Pack(self, value):
82 new_value = value.encode(self.encoding)
83 if len(new_value) >= self.max_length:
84 raise StructError('Value "%s" cannot be packed into field %s (len=%s)' %
85 (value, self.name, self.max_length))
86 return new_value
87
88 def Unpack(self, value):
89 return value.decode(self.encoding).strip('\x00')
Hung-Te Linc772e1a2017-04-14 16:50:50 +080090
Hung-Te Linbf8aa272018-04-19 03:02:29 +080091
92class GUID(uuid.UUID):
93 """A special UUID that defaults to upper case in str()."""
94
95 def __str__(self):
96 """Returns GUID in upper case."""
97 return super(GUID, self).__str__().upper()
98
99 @staticmethod
100 def Random():
101 return uuid.uuid4()
102
103
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800104class GUIDStructField(StructField):
105 """A GUID field."""
106
107 def __init__(self, name):
108 super(GUIDStructField, self).__init__('16s', name)
109
110 def Pack(self, value):
111 if value is None:
Yilin Yang235e5982019-12-26 10:36:22 +0800112 return b'\x00' * 16
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800113 if not isinstance(value, uuid.UUID):
114 raise StructError('Field %s needs a GUID value instead of [%r].' %
115 (self.name, value))
116 return value.bytes_le
117
118 def Unpack(self, value):
119 return GUID(bytes_le=value)
120
121
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800122def BitProperty(getter, setter, shift, mask):
123 """A generator for bit-field properties.
124
125 This is used inside a class to manipulate an integer-like variable using
126 properties. The getter and setter should be member functions to change the
127 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800128
129 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800130 getter: a function to read integer type variable (for all the bits).
131 setter: a function to set the new changed integer type variable.
132 shift: integer for how many bits should be shifted (right).
133 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800134 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800135 def _getter(self):
136 return (getter(self) >> shift) & mask
137 def _setter(self, value):
138 assert value & mask == value, (
139 'Value %s out of range (mask=%s)' % (value, mask))
140 setter(self, getter(self) & ~(mask << shift) | value << shift)
141 return property(_getter, _setter)
142
143
Fei Shaobd07c9a2020-06-15 19:04:50 +0800144class PartitionAttributes:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800145 """Wrapper for Partition.Attributes.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800146
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800147 This can be created using Partition.attrs, but the changed properties won't
148 apply to underlying Partition until an explicit call with
149 Partition.Update(Attributes=new_attrs).
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800150 """
151
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800152 def __init__(self, attrs):
153 self._attrs = attrs
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800154
155 @property
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800156 def raw(self):
157 """Returns the raw integer type attributes."""
158 return self._Get()
159
160 def _Get(self):
161 return self._attrs
162
163 def _Set(self, value):
164 self._attrs = value
165
166 successful = BitProperty(_Get, _Set, 56, 1)
167 tries = BitProperty(_Get, _Set, 52, 0xf)
168 priority = BitProperty(_Get, _Set, 48, 0xf)
169 legacy_boot = BitProperty(_Get, _Set, 2, 1)
170 required = BitProperty(_Get, _Set, 0, 1)
171 raw_16 = BitProperty(_Get, _Set, 48, 0xffff)
172
173
174class PartitionAttributeStructField(StructField):
175
176 def Pack(self, value):
177 if not isinstance(value, PartitionAttributes):
178 raise StructError('Given value %r is not %s.' %
179 (value, PartitionAttributes.__name__))
180 return value.raw
181
182 def Unpack(self, value):
183 return PartitionAttributes(value)
184
185
Yilin Yang9cf532e2019-12-13 12:02:59 +0800186# The binascii.crc32 returns unsigned integer in python3, so CRC32 in struct
187# must be declared as 'unsigned' (L).
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800188# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
189HEADER_FIELDS = [
190 StructField('8s', 'Signature'),
191 StructField('4s', 'Revision'),
192 StructField('L', 'HeaderSize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800193 StructField('L', 'CRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800194 StructField('4s', 'Reserved'),
195 StructField('Q', 'CurrentLBA'),
196 StructField('Q', 'BackupLBA'),
197 StructField('Q', 'FirstUsableLBA'),
198 StructField('Q', 'LastUsableLBA'),
199 GUIDStructField('DiskGUID'),
200 StructField('Q', 'PartitionEntriesStartingLBA'),
201 StructField('L', 'PartitionEntriesNumber'),
202 StructField('L', 'PartitionEntrySize'),
Yilin Yang9cf532e2019-12-13 12:02:59 +0800203 StructField('L', 'PartitionArrayCRC32'),
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800204]
205
206# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
207PARTITION_FIELDS = [
208 GUIDStructField('TypeGUID'),
209 GUIDStructField('UniqueGUID'),
210 StructField('Q', 'FirstLBA'),
211 StructField('Q', 'LastLBA'),
212 PartitionAttributeStructField('Q', 'Attributes'),
213 UTF16StructField(72, 'Names'),
214]
215
216# The PMBR has so many variants. The basic format is defined in
217# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout, and our
218# implementation, as derived from `cgpt`, is following syslinux as:
Stimim Chen38241b42020-12-08 13:07:34 +0800219# https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+/HEAD/cgpt/cgpt.h#32
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800220PMBR_FIELDS = [
221 StructField('424s', 'BootCode'),
222 GUIDStructField('BootGUID'),
223 StructField('L', 'DiskID'),
224 StructField('2s', 'Magic'),
225 StructField('16s', 'LegacyPart0'),
226 StructField('16s', 'LegacyPart1'),
227 StructField('16s', 'LegacyPart2'),
228 StructField('16s', 'LegacyPart3'),
229 StructField('2s', 'Signature'),
230]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800231
232
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800233class GPTError(Exception):
234 """All exceptions by GPT."""
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800235
236
Fei Shaobd07c9a2020-06-15 19:04:50 +0800237class GPTObject:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800238 """A base object in GUID Partition Table.
239
240 All objects (for instance, header or partition entries) must inherit this
241 class and define the FIELD attribute with a list of field definitions using
242 StructField.
243
244 The 'name' in StructField will become the attribute name of GPT objects that
245 can be directly packed into / unpacked from. Derived (calculated from existing
246 attributes) attributes should be in lower_case.
247
248 It is also possible to attach some additional properties to the object as meta
249 data (for example path of the underlying image file). To do that, first
250 include it in __slots__ list and specify them as dictionary-type args in
251 constructors. These properties will be preserved when you call Clone().
252
253 To create a new object, call the constructor. Field data can be assigned as
254 in arguments, or give nothing to initialize as zero (see Zero()). Field data
255 and meta values can be also specified in keyword arguments (**kargs) at the
256 same time.
257
258 To read a object from file or stream, use class method ReadFrom(source).
259 To make changes, modify the field directly or use Update(dict), or create a
260 copy by Clone() first then Update.
261
262 To wipe all fields (but not meta), call Zero(). There is currently no way
263 to clear meta except setting them to None one by one.
264 """
265 __slots__ = []
266
Peter Shih533566a2018-09-05 17:48:03 +0800267 FIELDS = []
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800268 """A list of StructField definitions."""
269
270 def __init__(self, *args, **kargs):
271 if args:
272 if len(args) != len(self.FIELDS):
273 raise GPTError('%s need %s arguments (found %s).' %
274 (type(self).__name__, len(self.FIELDS), len(args)))
275 for f, value in zip(self.FIELDS, args):
276 setattr(self, f.name, value)
277 else:
278 self.Zero()
279
Yilin Yang4c74ca02020-07-22 16:12:01 +0800280 all_names = list(self.__slots__)
Yilin Yang879fbda2020-05-14 13:52:30 +0800281 for name, value in kargs.items():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800282 if name not in all_names:
283 raise GPTError('%s does not support keyword arg <%s>.' %
284 (type(self).__name__, name))
285 setattr(self, name, value)
286
287 def __iter__(self):
288 """An iterator to return all fields associated in the object."""
289 return (getattr(self, f.name) for f in self.FIELDS)
290
291 def __repr__(self):
292 return '(%s: %s)' % (type(self).__name__, ', '.join(
Peter Shihe6afab32018-09-11 17:16:48 +0800293 '%s=%r' % (f, getattr(self, f)) for f in self.__slots__))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800294
295 @classmethod
296 def GetStructFormat(cls):
297 """Returns a format string for struct to use."""
298 return '<' + ''.join(f.fmt for f in cls.FIELDS)
299
300 @classmethod
301 def ReadFrom(cls, source, **kargs):
302 """Returns an object from given source."""
303 obj = cls(**kargs)
304 obj.Unpack(source)
305 return obj
306
307 @property
308 def blob(self):
309 """The (packed) blob representation of the object."""
310 return self.Pack()
311
312 @property
313 def meta(self):
314 """Meta values (those not in GPT object fields)."""
Fei Shaoa161e752020-06-16 18:14:51 +0800315 metas = set(self.__slots__) - {f.name for f in self.FIELDS}
Fei Shao95075802020-06-16 16:55:25 +0800316 return {name: getattr(self, name) for name in metas}
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800317
318 def Unpack(self, source):
319 """Unpacks values from a given source.
320
321 Args:
322 source: a string of bytes or a file-like object to read from.
323 """
324 fmt = self.GetStructFormat()
325 if source is None:
326 source = '\x00' * struct.calcsize(fmt)
Yilin Yang235e5982019-12-26 10:36:22 +0800327 if not isinstance(source, (str, bytes)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800328 return self.Unpack(source.read(struct.calcsize(fmt)))
Yilin Yang235e5982019-12-26 10:36:22 +0800329 if isinstance(source, str):
330 source = source.encode('utf-8')
331 for f, value in zip(self.FIELDS, struct.unpack(fmt.encode('utf-8'),
332 source)):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800333 setattr(self, f.name, f.Unpack(value))
Yilin Yang840fdc42020-01-16 16:37:42 +0800334 return None
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800335
336 def Pack(self):
Yilin Yang235e5982019-12-26 10:36:22 +0800337 """Packs values in all fields into a bytes by struct format."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800338 return struct.pack(self.GetStructFormat(),
339 *(f.Pack(getattr(self, f.name)) for f in self.FIELDS))
340
341 def Clone(self):
342 """Clones a new instance."""
343 return type(self)(*self, **self.meta)
344
345 def Update(self, **dargs):
346 """Applies multiple values in current object."""
Yilin Yang879fbda2020-05-14 13:52:30 +0800347 for name, value in dargs.items():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800348 setattr(self, name, value)
349
350 def Zero(self):
351 """Set all fields to values representing zero or empty.
352
353 Note the meta attributes won't be cleared.
354 """
Fei Shaobd07c9a2020-06-15 19:04:50 +0800355 class ZeroReader:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800356 """A /dev/zero like stream."""
357
358 @staticmethod
359 def read(num):
360 return '\x00' * num
361
362 self.Unpack(ZeroReader())
363
364
Fei Shaobd07c9a2020-06-15 19:04:50 +0800365class GPT:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800366 """A GPT helper class.
367
368 To load GPT from an existing disk image file, use `LoadFromFile`.
369 After modifications were made, use `WriteToFile` to commit changes.
370
371 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800372 header: a namedtuple of GPT header.
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800373 pmbr: a namedtuple of Protective MBR.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800374 partitions: a list of GPT partition entry nametuple.
375 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800376 is_secondary: boolean to indicate if the header is from primary or backup.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800377 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800378 DEFAULT_BLOCK_SIZE = 512
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800379 # Old devices uses 'Basic data' type for stateful partition, and newer devices
380 # should use 'Linux (fS) data' type; so we added a 'stateful' suffix for
381 # migration.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800382 TYPE_GUID_MAP = {
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800383 GUID('00000000-0000-0000-0000-000000000000'): 'Unused',
Hung-Te Lin43d54c12019-03-22 11:15:59 +0800384 GUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'): 'Basic data stateful',
385 GUID('0FC63DAF-8483-4772-8E79-3D69D8477DE4'): 'Linux data',
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800386 GUID('FE3A2A5D-4F32-41A7-B725-ACCC3285A309'): 'ChromeOS kernel',
387 GUID('3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC'): 'ChromeOS rootfs',
388 GUID('2E0A753D-9E48-43B0-8337-B15192CB1B5E'): 'ChromeOS reserved',
389 GUID('CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3'): 'ChromeOS firmware',
390 GUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B'): 'EFI System Partition',
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800391 }
Fei Shao95075802020-06-16 16:55:25 +0800392 TYPE_GUID_FROM_NAME = {
393 'efi' if v.startswith('EFI') else v.lower().split()[-1]: k
394 for k, v in TYPE_GUID_MAP.items()}
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800395 TYPE_GUID_UNUSED = TYPE_GUID_FROM_NAME['unused']
396 TYPE_GUID_CHROMEOS_KERNEL = TYPE_GUID_FROM_NAME['kernel']
397 TYPE_GUID_LIST_BOOTABLE = [
398 TYPE_GUID_CHROMEOS_KERNEL,
399 TYPE_GUID_FROM_NAME['efi'],
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800400 ]
401
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800402 class ProtectiveMBR(GPTObject):
403 """Protective MBR (PMBR) in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800404 FIELDS = PMBR_FIELDS
405 __slots__ = [f.name for f in FIELDS]
406
Yilin Yang235e5982019-12-26 10:36:22 +0800407 SIGNATURE = b'\x55\xAA'
408 MAGIC = b'\x1d\x9a'
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800409
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800410 class Header(GPTObject):
411 """Wrapper to Header in GPT."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800412 FIELDS = HEADER_FIELDS
413 __slots__ = [f.name for f in FIELDS]
414
Yilin Yang235e5982019-12-26 10:36:22 +0800415 SIGNATURES = [b'EFI PART', b'CHROMEOS']
416 SIGNATURE_IGNORE = b'IGNOREME'
417 DEFAULT_REVISION = b'\x00\x00\x01\x00'
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800418
419 DEFAULT_PARTITION_ENTRIES = 128
420 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
421
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800422 @classmethod
423 def Create(cls, size, block_size, pad_blocks=0,
424 part_entries=DEFAULT_PARTITION_ENTRIES):
425 """Creates a header with default values.
426
427 Args:
428 size: integer of expected image size.
429 block_size: integer for size of each block (sector).
430 pad_blocks: number of preserved sectors between header and partitions.
431 part_entries: number of partitions to include in header.
432 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800433 PART_FORMAT = GPT.Partition.GetStructFormat()
434 FORMAT = cls.GetStructFormat()
435 part_entry_size = struct.calcsize(PART_FORMAT)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800436 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
437 parts_bytes = part_entries * part_entry_size
Yilin Yang14d02a22019-11-01 11:32:03 +0800438 parts_blocks = parts_bytes // block_size
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800439 if parts_bytes % block_size:
440 parts_blocks += 1
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800441 # CRC32 and PartitionsCRC32 must be updated later explicitly.
442 return cls(
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800443 Signature=cls.SIGNATURES[0],
444 Revision=cls.DEFAULT_REVISION,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800445 HeaderSize=struct.calcsize(FORMAT),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800446 CurrentLBA=1,
Yilin Yang14d02a22019-11-01 11:32:03 +0800447 BackupLBA=size // block_size - 1,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800448 FirstUsableLBA=parts_lba + parts_blocks,
Yilin Yang14d02a22019-11-01 11:32:03 +0800449 LastUsableLBA=size // block_size - parts_blocks - parts_lba,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800450 DiskGUID=GUID.Random(),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800451 PartitionEntriesStartingLBA=parts_lba,
452 PartitionEntriesNumber=part_entries,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800453 PartitionEntrySize=part_entry_size)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800454
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800455 def UpdateChecksum(self):
456 """Updates the CRC32 field in GPT header.
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800457
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800458 Note the PartitionArrayCRC32 is not touched - you have to make sure that
459 is correct before calling Header.UpdateChecksum().
460 """
461 self.Update(CRC32=0)
462 self.Update(CRC32=binascii.crc32(self.blob))
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800463
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800464 class Partition(GPTObject):
465 """The partition entry in GPT.
466
467 Please include following properties when creating a Partition object:
468 - image: a string for path to the image file the partition maps to.
469 - number: the 1-based partition number.
470 - block_size: an integer for size of each block (LBA, or sector).
471 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800472 FIELDS = PARTITION_FIELDS
473 __slots__ = [f.name for f in FIELDS] + ['image', 'number', 'block_size']
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800474 NAMES_ENCODING = 'utf-16-le'
475 NAMES_LENGTH = 72
476
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800477 def __str__(self):
478 return '%s#%s' % (self.image, self.number)
479
480 def IsUnused(self):
481 """Returns if the partition is unused and can be allocated."""
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800482 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800483
Hung-Te Linfe724f82018-04-18 15:03:58 +0800484 def IsChromeOSKernel(self):
485 """Returns if the partition is a Chrome OS kernel partition."""
Hung-Te Lin048ac5e2018-05-03 23:47:45 +0800486 return self.TypeGUID == GPT.TYPE_GUID_CHROMEOS_KERNEL
Hung-Te Linfe724f82018-04-18 15:03:58 +0800487
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800488 @property
489 def blocks(self):
490 """Return size of partition in blocks (see block_size)."""
491 return self.LastLBA - self.FirstLBA + 1
492
493 @property
494 def offset(self):
495 """Returns offset to partition in bytes."""
496 return self.FirstLBA * self.block_size
497
498 @property
499 def size(self):
500 """Returns size of partition in bytes."""
501 return self.blocks * self.block_size
502
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800503 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800504 """GPT constructor.
505
506 See LoadFromFile for how it's usually used.
507 """
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800508 self.pmbr = None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800509 self.header = None
510 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800511 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800512 self.is_secondary = False
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800513
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800514 @classmethod
Hung-Te Linbf8aa272018-04-19 03:02:29 +0800515 def GetTypeGUID(cls, value):
516 """The value may be a GUID in string or a short type string."""
517 guid = cls.TYPE_GUID_FROM_NAME.get(value.lower())
518 return GUID(value) if guid is None else guid
Hung-Te Linf641d302018-04-18 15:09:35 +0800519
520 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800521 def Create(cls, image_name, size, block_size, pad_blocks=0):
522 """Creates a new GPT instance from given size and block_size.
523
524 Args:
525 image_name: a string of underlying disk image file name.
526 size: expected size of disk image.
527 block_size: size of each block (sector) in bytes.
528 pad_blocks: number of blocks between header and partitions array.
529 """
530 gpt = cls()
531 gpt.block_size = block_size
532 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
533 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800534 cls.Partition(block_size=block_size, image=image_name, number=i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800535 for i in range(gpt.header.PartitionEntriesNumber)]
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800536 return gpt
537
Hung-Te Lin446eb512018-05-02 18:39:16 +0800538 @staticmethod
539 def IsBlockDevice(image):
540 """Returns if the image is a block device file."""
541 return stat.S_ISBLK(os.stat(image).st_mode)
542
543 @classmethod
544 def GetImageSize(cls, image):
545 """Returns the size of specified image (plain or block device file)."""
546 if not cls.IsBlockDevice(image):
547 return os.path.getsize(image)
548
549 fd = os.open(image, os.O_RDONLY)
550 try:
551 return os.lseek(fd, 0, os.SEEK_END)
552 finally:
553 os.close(fd)
554
555 @classmethod
556 def GetLogicalBlockSize(cls, block_dev):
557 """Returns the logical block (sector) size from a block device file.
558
559 The underlying call is BLKSSZGET. An alternative command is blockdev,
560 but that needs root permission even if we just want to get sector size.
561 """
562 assert cls.IsBlockDevice(block_dev), '%s must be block device.' % block_dev
563 return int(subprocess.check_output(
564 ['lsblk', '-d', '-n', '-r', '-o', 'log-sec', block_dev]).strip())
565
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800566 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800567 def LoadFromFile(cls, image):
568 """Loads a GPT table from give disk image file object.
569
570 Args:
571 image: a string as file path or a file-like object to read from.
572 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800573 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800574 with open(image, 'rb') as f:
575 return cls.LoadFromFile(f)
576
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800577 gpt = cls()
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800578 image.seek(0)
579 pmbr = gpt.ProtectiveMBR.ReadFrom(image)
580 if pmbr.Signature == cls.ProtectiveMBR.SIGNATURE:
581 logging.debug('Found MBR signature in %s', image.name)
582 if pmbr.Magic == cls.ProtectiveMBR.MAGIC:
583 logging.debug('Found PMBR in %s', image.name)
584 gpt.pmbr = pmbr
585
Hung-Te Linf148d322018-04-13 10:24:42 +0800586 # Try DEFAULT_BLOCK_SIZE, then 4K.
Hung-Te Lin446eb512018-05-02 18:39:16 +0800587 block_sizes = [cls.DEFAULT_BLOCK_SIZE, 4096]
588 if cls.IsBlockDevice(image.name):
589 block_sizes = [cls.GetLogicalBlockSize(image.name)]
590
591 for block_size in block_sizes:
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800592 # Note because there are devices setting Primary as ignored and the
593 # partition table signature accepts 'CHROMEOS' which is also used by
594 # Chrome OS kernel partition, we have to look for Secondary (backup) GPT
595 # first before trying other block sizes, otherwise we may incorrectly
596 # identify a kernel partition as LBA 1 of larger block size system.
597 for i, seek in enumerate([(block_size * 1, os.SEEK_SET),
598 (-block_size, os.SEEK_END)]):
599 image.seek(*seek)
600 header = gpt.Header.ReadFrom(image)
601 if header.Signature in cls.Header.SIGNATURES:
602 gpt.block_size = block_size
603 if i != 0:
604 gpt.is_secondary = True
605 break
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800606 # TODO(hungte) Try harder to see if this block is valid.
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800607 else:
608 # Nothing found, try next block size.
609 continue
610 # Found a valid signature.
611 break
Hung-Te Linf148d322018-04-13 10:24:42 +0800612 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800613 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800614
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800615 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800616 def ReadPartition(image, number):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800617 p = gpt.Partition.ReadFrom(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800618 image, image=image.name, number=number, block_size=gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800619 return p
620
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800621 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800622 gpt.partitions = [
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800623 ReadPartition(image, i + 1)
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800624 for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800625 return gpt
626
Hung-Te Linc5196682018-04-18 22:59:59 +0800627 def GetUsedPartitions(self):
628 """Returns a list of partitions with type GUID not set to unused.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800629
Hung-Te Linc5196682018-04-18 22:59:59 +0800630 Use 'number' property to find the real location of partition in
631 self.partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800632 """
Hung-Te Linc5196682018-04-18 22:59:59 +0800633 return [p for p in self.partitions if not p.IsUnused()]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800634
635 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800636 """Returns the max LastLBA from all used partitions."""
Hung-Te Linc5196682018-04-18 22:59:59 +0800637 parts = self.GetUsedPartitions()
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800638 return (max(p.LastLBA for p in parts)
639 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800640
641 def GetPartitionTableBlocks(self, header=None):
642 """Returns the blocks (or LBA) of partition table from given header."""
643 if header is None:
644 header = self.header
645 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Yilin Yang14d02a22019-11-01 11:32:03 +0800646 blocks = size // self.block_size
Hung-Te Linf148d322018-04-13 10:24:42 +0800647 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800648 blocks += 1
649 return blocks
650
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800651 def GetPartition(self, number):
652 """Gets the Partition by given (1-based) partition number.
653
654 Args:
655 number: an integer as 1-based partition number.
656 """
657 if not 0 < number <= len(self.partitions):
658 raise GPTError('Invalid partition number %s.' % number)
659 return self.partitions[number - 1]
660
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800661 def UpdatePartition(self, part, number):
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800662 """Updates the entry in partition table by given Partition object.
663
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800664 Usually you only need to call this if you want to copy one partition to
665 different location (number of image).
666
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800667 Args:
668 part: a Partition GPT object.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800669 number: an integer as 1-based partition number.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800670 """
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800671 ref = self.partitions[number - 1]
672 part = part.Clone()
673 part.number = number
674 part.image = ref.image
675 part.block_size = ref.block_size
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800676 self.partitions[number - 1] = part
677
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800678 def GetSize(self):
679 return self.block_size * (self.header.BackupLBA + 1)
680
681 def Resize(self, new_size, check_overlap=True):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800682 """Adjust GPT for a disk image in given size.
683
684 Args:
685 new_size: Integer for new size of disk image file.
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800686 check_overlap: Checks if the backup partition table overlaps used
687 partitions.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800688 """
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800689 old_size = self.GetSize()
Hung-Te Linf148d322018-04-13 10:24:42 +0800690 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800691 raise GPTError(
692 'New file size %d is not valid for image files.' % new_size)
Yilin Yang14d02a22019-11-01 11:32:03 +0800693 new_blocks = new_size // self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800694 if old_size != new_size:
Yilin Yang9881b1e2019-12-11 11:47:33 +0800695 logging.warning('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
696 new_size, new_blocks, old_size,
697 old_size // self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800698 else:
699 logging.info('Image size (%d, LBA=%d) not changed.',
700 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800701 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800702
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800703 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800704 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800705 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800706
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800707 if check_overlap and last_usable_lba < self.header.LastUsableLBA:
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800708 max_used_lba = self.GetMaxUsedLBA()
709 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800710 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800711
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800712 self.header.Update(BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800713
714 def GetFreeSpace(self):
715 """Returns the free (available) space left according to LastUsableLBA."""
716 max_lba = self.GetMaxUsedLBA()
717 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800718 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800719
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800720 def ExpandPartition(self, number):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800721 """Expands a given partition to last usable LBA.
722
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800723 The size of the partition can actually be reduced if the last usable LBA
724 decreases.
725
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800726 Args:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800727 number: an integer to specify partition in 1-based number.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800728
729 Returns:
730 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800731 """
732 # Assume no partitions overlap, we need to make sure partition[i] has
733 # largest LBA.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +0800734 p = self.GetPartition(number)
735 if p.IsUnused():
736 raise GPTError('Partition %s is unused.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800737 max_used_lba = self.GetMaxUsedLBA()
Hung-Te Linc5196682018-04-18 22:59:59 +0800738 # TODO(hungte) We can do more by finding free space after i.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800739 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800740 raise GPTError(
Hung-Te Linc5196682018-04-18 22:59:59 +0800741 'Cannot expand %s because it is not allocated at last.' % p)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800742
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800743 old_blocks = p.blocks
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800744 p.Update(LastLBA=self.header.LastUsableLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800745 new_blocks = p.blocks
Yilin Yang9881b1e2019-12-11 11:47:33 +0800746 logging.warning(
Cheng-Han Yangdc235b32019-01-08 18:05:40 +0800747 '%s size changed in LBA: %d -> %d.', p, old_blocks, new_blocks)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800748 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800749
Hung-Te Lin3b491672018-04-19 01:41:20 +0800750 def CheckIntegrity(self):
751 """Checks if the GPT objects all look good."""
752 # Check if the header allocation looks good. CurrentLBA and
753 # PartitionEntriesStartingLBA should be all outside [FirstUsableLBA,
754 # LastUsableLBA].
755 header = self.header
756 entries_first_lba = header.PartitionEntriesStartingLBA
757 entries_last_lba = entries_first_lba + self.GetPartitionTableBlocks() - 1
758
759 def CheckOutsideUsable(name, lba, outside_entries=False):
760 if lba < 1:
761 raise GPTError('%s should not live in LBA %s.' % (name, lba))
762 if lba > max(header.BackupLBA, header.CurrentLBA):
763 # Note this is "in theory" possible, but we want to report this as
764 # error as well, since it usually leads to error.
765 raise GPTError('%s (%s) should not be larger than BackupLBA (%s).' %
766 (name, lba, header.BackupLBA))
767 if header.FirstUsableLBA <= lba <= header.LastUsableLBA:
768 raise GPTError('%s (%s) should not be included in usable LBAs [%s,%s]' %
769 (name, lba, header.FirstUsableLBA, header.LastUsableLBA))
770 if outside_entries and entries_first_lba <= lba <= entries_last_lba:
771 raise GPTError('%s (%s) should be outside partition entries [%s,%s]' %
772 (name, lba, entries_first_lba, entries_last_lba))
773 CheckOutsideUsable('Header', header.CurrentLBA, True)
774 CheckOutsideUsable('Backup header', header.BackupLBA, True)
775 CheckOutsideUsable('Partition entries', entries_first_lba)
776 CheckOutsideUsable('Partition entries end', entries_last_lba)
777
778 parts = self.GetUsedPartitions()
779 # Check if partition entries overlap with each other.
780 lba_list = [(p.FirstLBA, p.LastLBA, p) for p in parts]
781 lba_list.sort(key=lambda t: t[0])
Yilin Yangbf84d2e2020-05-13 10:34:46 +0800782 for i in range(len(lba_list) - 1):
Hung-Te Lin3b491672018-04-19 01:41:20 +0800783 if lba_list[i][1] >= lba_list[i + 1][0]:
784 raise GPTError('Overlap in partition entries: [%s,%s]%s, [%s,%s]%s.' %
785 (lba_list[i] + lba_list[i + 1]))
786 # Now, check the first and last partition.
787 if lba_list:
788 p = lba_list[0][2]
789 if p.FirstLBA < header.FirstUsableLBA:
790 raise GPTError(
791 'Partition %s must not go earlier (%s) than FirstUsableLBA=%s' %
792 (p, p.FirstLBA, header.FirstLBA))
793 p = lba_list[-1][2]
794 if p.LastLBA > header.LastUsableLBA:
795 raise GPTError(
796 'Partition %s must not go further (%s) than LastUsableLBA=%s' %
797 (p, p.LastLBA, header.LastLBA))
798 # Check if UniqueGUIDs are not unique.
799 if len(set(p.UniqueGUID for p in parts)) != len(parts):
800 raise GPTError('Partition UniqueGUIDs are duplicated.')
801 # Check if CRCs match.
Yilin Yang235e5982019-12-26 10:36:22 +0800802 if (binascii.crc32(b''.join(p.blob for p in self.partitions)) !=
Hung-Te Lin3b491672018-04-19 01:41:20 +0800803 header.PartitionArrayCRC32):
804 raise GPTError('GPT Header PartitionArrayCRC32 does not match.')
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800805 header_crc = header.Clone()
806 header_crc.UpdateChecksum()
807 if header_crc.CRC32 != header.CRC32:
808 raise GPTError('GPT Header CRC32 does not match.')
Hung-Te Lin3b491672018-04-19 01:41:20 +0800809
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800810 def UpdateChecksum(self):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800811 """Updates all checksum fields in GPT objects."""
Yilin Yang235e5982019-12-26 10:36:22 +0800812 parts = b''.join(p.blob for p in self.partitions)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800813 self.header.Update(PartitionArrayCRC32=binascii.crc32(parts))
814 self.header.UpdateChecksum()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800815
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800816 def GetBackupHeader(self, header):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800817 """Returns the backup header according to given header.
818
819 This should be invoked only after GPT.UpdateChecksum() has updated all CRC32
820 fields.
821 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800822 partitions_starting_lba = (
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800823 header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800824 h = header.Clone()
825 h.Update(
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800826 BackupLBA=header.CurrentLBA,
827 CurrentLBA=header.BackupLBA,
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800828 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800829 h.UpdateChecksum()
830 return h
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800831
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800832 @classmethod
833 def WriteProtectiveMBR(cls, image, create, bootcode=None, boot_guid=None):
834 """Writes a protective MBR to given file.
835
836 Each MBR is 512 bytes: 424 bytes for bootstrap code, 16 bytes of boot GUID,
837 4 bytes of disk id, 2 bytes of bootcode magic, 4*16 for 4 partitions, and 2
838 byte as signature. cgpt has hard-coded the CHS and bootstrap magic values so
839 we can follow that.
840
841 Args:
842 create: True to re-create PMBR structure.
843 bootcode: a blob of new boot code.
844 boot_guid a blob for new boot GUID.
845
846 Returns:
847 The written PMBR structure.
848 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800849 if isinstance(image, str):
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800850 with open(image, 'rb+') as f:
851 return cls.WriteProtectiveMBR(f, create, bootcode, boot_guid)
852
853 image.seek(0)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800854 pmbr_format = cls.ProtectiveMBR.GetStructFormat()
855 assert struct.calcsize(pmbr_format) == cls.DEFAULT_BLOCK_SIZE
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800856 pmbr = cls.ProtectiveMBR.ReadFrom(image)
857
858 if create:
859 legacy_sectors = min(
860 0x100000000,
Yilin Yang14d02a22019-11-01 11:32:03 +0800861 GPT.GetImageSize(image.name) // cls.DEFAULT_BLOCK_SIZE) - 1
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800862 # Partition 0 must have have the fixed CHS with number of sectors
863 # (calculated as legacy_sectors later).
Yilin Yangf9fe1932019-11-04 17:09:34 +0800864 part0 = (codecs.decode('00000200eeffffff01000000', 'hex') +
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800865 struct.pack('<I', legacy_sectors))
866 # Partition 1~3 should be all zero.
867 part1 = '\x00' * 16
868 assert len(part0) == len(part1) == 16, 'MBR entry is wrong.'
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800869 pmbr.Update(
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800870 BootGUID=cls.TYPE_GUID_UNUSED,
871 DiskID=0,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800872 Magic=pmbr.MAGIC,
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800873 LegacyPart0=part0,
874 LegacyPart1=part1,
875 LegacyPart2=part1,
876 LegacyPart3=part1,
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800877 Signature=pmbr.SIGNATURE)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800878
879 if bootcode:
880 if len(bootcode) > len(pmbr.BootCode):
881 logging.info(
882 'Bootcode is larger (%d > %d)!', len(bootcode), len(pmbr.BootCode))
883 bootcode = bootcode[:len(pmbr.BootCode)]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800884 pmbr.Update(BootCode=bootcode)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800885 if boot_guid:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +0800886 pmbr.Update(BootGUID=boot_guid)
Hung-Te Linc6e009c2018-04-17 15:06:16 +0800887
888 blob = pmbr.blob
889 assert len(blob) == cls.DEFAULT_BLOCK_SIZE
890 image.seek(0)
891 image.write(blob)
892 return pmbr
893
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800894 def WriteToFile(self, image):
895 """Updates partition table in a disk image file.
896
897 Args:
898 image: a string as file path or a file-like object to write into.
899 """
Yilin Yang0724c9d2019-11-15 15:53:45 +0800900 if isinstance(image, str):
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800901 with open(image, 'rb+') as f:
902 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800903
904 def WriteData(name, blob, lba):
905 """Writes a blob into given location."""
906 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800907 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800908 image.seek(lba * self.block_size)
909 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800910
911 self.UpdateChecksum()
Hung-Te Lin3b491672018-04-19 01:41:20 +0800912 self.CheckIntegrity()
Yilin Yang235e5982019-12-26 10:36:22 +0800913 parts_blob = b''.join(p.blob for p in self.partitions)
Hung-Te Linc34d89c2018-04-17 15:11:34 +0800914
915 header = self.header
916 WriteData('GPT Header', header.blob, header.CurrentLBA)
917 WriteData('GPT Partitions', parts_blob, header.PartitionEntriesStartingLBA)
918 logging.info(
919 'Usable LBA: First=%d, Last=%d', header.FirstUsableLBA,
920 header.LastUsableLBA)
921
922 if not self.is_secondary:
923 # When is_secondary is True, the header we have is actually backup header.
924 backup_header = self.GetBackupHeader(self.header)
925 WriteData(
926 'Backup Partitions', parts_blob,
927 backup_header.PartitionEntriesStartingLBA)
928 WriteData(
929 'Backup Header', backup_header.blob, backup_header.CurrentLBA)
Yilin Yang840fdc42020-01-16 16:37:42 +0800930 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800931
932
Fei Shaobd07c9a2020-06-15 19:04:50 +0800933class GPTCommands:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800934 """Collection of GPT sub commands for command line to use.
935
936 The commands are derived from `cgpt`, but not necessary to be 100% compatible
937 with cgpt.
938 """
939
940 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800941 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800942 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800943 ('type', 'type guid'),
944 ('unique', 'unique guid'),
945 ('label', 'label'),
946 ('Successful', 'Successful flag'),
947 ('Tries', 'Tries flag'),
948 ('Priority', 'Priority flag'),
949 ('Legacy', 'Legacy Boot flag'),
950 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800951
952 def __init__(self):
Fei Shao95075802020-06-16 16:55:25 +0800953 commands = {
954 command.lower(): getattr(self, command)()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800955 for command in dir(self)
956 if (isinstance(getattr(self, command), type) and
957 issubclass(getattr(self, command), self.SubCommand) and
958 getattr(self, command) is not self.SubCommand)
Fei Shao95075802020-06-16 16:55:25 +0800959 }
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800960 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800961
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800962 def DefineArgs(self, parser):
963 """Defines all available commands to an argparser subparsers instance."""
Cheng Yueh1d686a72021-03-10 11:09:42 +0800964 subparsers = parser.add_subparsers(title='subcommands',
965 help='Sub-command help.', dest='command')
966 subparsers.required = True
Yilin Yang879fbda2020-05-14 13:52:30 +0800967 for name, instance in sorted(self.commands.items()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800968 parser = subparsers.add_parser(
969 name, description=instance.__doc__,
970 formatter_class=argparse.RawDescriptionHelpFormatter,
971 help=instance.__doc__.splitlines()[0])
972 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800973
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800974 def Execute(self, args):
975 """Execute the sub commands by given parsed arguments."""
Hung-Te Linf641d302018-04-18 15:09:35 +0800976 return self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800977
Fei Shaobd07c9a2020-06-15 19:04:50 +0800978 class SubCommand:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800979 """A base class for sub commands to derive from."""
980
981 def DefineArgs(self, parser):
982 """Defines command line arguments to argparse parser.
983
984 Args:
985 parser: An argparse parser instance.
986 """
987 del parser # Unused.
988 raise NotImplementedError
989
990 def Execute(self, args):
Hung-Te Line0d1fa72018-05-15 00:04:48 +0800991 """Execute the command with parsed arguments.
992
993 To execute with raw arguments, use ExecuteCommandLine instead.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800994
995 Args:
996 args: An argparse parsed namespace.
997 """
998 del args # Unused.
999 raise NotImplementedError
1000
Hung-Te Line0d1fa72018-05-15 00:04:48 +08001001 def ExecuteCommandLine(self, *args):
1002 """Execute as invoked from command line.
1003
1004 This provides an easy way to execute particular sub command without
1005 creating argument parser explicitly.
1006
1007 Args:
1008 args: a list of string type command line arguments.
1009 """
1010 parser = argparse.ArgumentParser()
1011 self.DefineArgs(parser)
1012 return self.Execute(parser.parse_args(args))
1013
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001014 class Create(SubCommand):
1015 """Create or reset GPT headers and tables.
1016
1017 Create or reset an empty GPT.
1018 """
1019
1020 def DefineArgs(self, parser):
1021 parser.add_argument(
1022 '-z', '--zero', action='store_true',
1023 help='Zero the sectors of the GPT table and entries')
1024 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001025 '-p', '--pad-blocks', type=int, default=0,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001026 help=('Size (in blocks) of the disk to pad between the '
1027 'primary GPT header and its entries, default %(default)s'))
1028 parser.add_argument(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001029 '--block_size', type=int,
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001030 help='Size of each block (sector) in bytes.')
1031 parser.add_argument(
1032 'image_file', type=argparse.FileType('rb+'),
1033 help='Disk image file to create.')
1034
1035 def Execute(self, args):
1036 block_size = args.block_size
Hung-Te Lin446eb512018-05-02 18:39:16 +08001037 if block_size is None:
1038 if GPT.IsBlockDevice(args.image_file.name):
1039 block_size = GPT.GetLogicalBlockSize(args.image_file.name)
1040 else:
1041 block_size = GPT.DEFAULT_BLOCK_SIZE
1042
1043 if block_size != GPT.DEFAULT_BLOCK_SIZE:
1044 logging.info('Block (sector) size for %s is set to %s bytes.',
1045 args.image_file.name, block_size)
1046
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001047 gpt = GPT.Create(
Hung-Te Lin446eb512018-05-02 18:39:16 +08001048 args.image_file.name, GPT.GetImageSize(args.image_file.name),
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001049 block_size, args.pad_blocks)
1050 if args.zero:
1051 # In theory we only need to clear LBA 1, but to make sure images already
1052 # initialized with different block size won't have GPT signature in
1053 # different locations, we should zero until first usable LBA.
1054 args.image_file.seek(0)
Yilin Yang235e5982019-12-26 10:36:22 +08001055 args.image_file.write(b'\0' * block_size * gpt.header.FirstUsableLBA)
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001056 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001057 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001058 return 'Created GPT for %s' % args.image_file.name
Hung-Te Lin6c3575a2018-04-17 15:00:49 +08001059
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001060 class Boot(SubCommand):
1061 """Edit the PMBR sector for legacy BIOSes.
1062
1063 With no options, it will just print the PMBR boot guid.
1064 """
1065
1066 def DefineArgs(self, parser):
1067 parser.add_argument(
1068 '-i', '--number', type=int,
1069 help='Set bootable partition')
1070 parser.add_argument(
Stimim Chen0e6071b2020-04-28 18:08:49 +08001071 '-b', '--bootloader', type=argparse.FileType('rb'),
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001072 help='Install bootloader code in the PMBR')
1073 parser.add_argument(
1074 '-p', '--pmbr', action='store_true',
1075 help='Create legacy PMBR partition table')
1076 parser.add_argument(
1077 'image_file', type=argparse.FileType('rb+'),
1078 help='Disk image file to change PMBR.')
1079
1080 def Execute(self, args):
1081 """Rebuilds the protective MBR."""
1082 bootcode = args.bootloader.read() if args.bootloader else None
1083 boot_guid = None
1084 if args.number is not None:
1085 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001086 boot_guid = gpt.GetPartition(args.number).UniqueGUID
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001087 pmbr = GPT.WriteProtectiveMBR(
1088 args.image_file, args.pmbr, bootcode=bootcode, boot_guid=boot_guid)
1089
You-Cheng Syufff7f422018-05-14 15:37:39 +08001090 print(pmbr.BootGUID)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001091 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001092 return 0
Hung-Te Linc6e009c2018-04-17 15:06:16 +08001093
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001094 class Legacy(SubCommand):
1095 """Switch between GPT and Legacy GPT.
1096
1097 Switch GPT header signature to "CHROMEOS".
1098 """
1099
1100 def DefineArgs(self, parser):
1101 parser.add_argument(
1102 '-e', '--efi', action='store_true',
1103 help='Switch GPT header signature back to "EFI PART"')
1104 parser.add_argument(
1105 '-p', '--primary-ignore', action='store_true',
1106 help='Switch primary GPT header signature to "IGNOREME"')
1107 parser.add_argument(
1108 'image_file', type=argparse.FileType('rb+'),
1109 help='Disk image file to change.')
1110
1111 def Execute(self, args):
1112 gpt = GPT.LoadFromFile(args.image_file)
1113 # cgpt behavior: if -p is specified, -e is ignored.
1114 if args.primary_ignore:
1115 if gpt.is_secondary:
1116 raise GPTError('Sorry, the disk already has primary GPT ignored.')
1117 args.image_file.seek(gpt.header.CurrentLBA * gpt.block_size)
1118 args.image_file.write(gpt.header.SIGNATURE_IGNORE)
1119 gpt.header = gpt.GetBackupHeader(self.header)
1120 gpt.is_secondary = True
1121 else:
1122 new_signature = gpt.Header.SIGNATURES[0 if args.efi else 1]
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001123 gpt.header.Update(Signature=new_signature)
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001124 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001125 args.image_file.close()
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001126 if args.primary_ignore:
Hung-Te Linbad46112018-05-15 16:39:14 +08001127 return ('Set %s primary GPT header to %s.' %
1128 (args.image_file.name, gpt.header.SIGNATURE_IGNORE))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001129 return ('Changed GPT signature for %s to %s.' %
1130 (args.image_file.name, new_signature))
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001131
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001132 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001133 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001134
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001135 def DefineArgs(self, parser):
1136 parser.add_argument(
1137 'image_file', type=argparse.FileType('rb+'),
1138 help='Disk image file to repair.')
1139
1140 def Execute(self, args):
1141 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin446eb512018-05-02 18:39:16 +08001142 gpt.Resize(GPT.GetImageSize(args.image_file.name))
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001143 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001144 args.image_file.close()
Hung-Te Linbad46112018-05-15 16:39:14 +08001145 return 'Disk image file %s repaired.' % args.image_file.name
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001146
1147 class Expand(SubCommand):
1148 """Expands a GPT partition to all available free space."""
1149
1150 def DefineArgs(self, parser):
1151 parser.add_argument(
1152 '-i', '--number', type=int, required=True,
1153 help='The partition to expand.')
1154 parser.add_argument(
1155 'image_file', type=argparse.FileType('rb+'),
1156 help='Disk image file to modify.')
1157
1158 def Execute(self, args):
1159 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001160 old_blocks, new_blocks = gpt.ExpandPartition(args.number)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001161 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001162 args.image_file.close()
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001163 if old_blocks < new_blocks:
Hung-Te Linbad46112018-05-15 16:39:14 +08001164 return (
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001165 'Partition %s on disk image file %s has been extended '
1166 'from %s to %s .' %
1167 (args.number, args.image_file.name, old_blocks * gpt.block_size,
1168 new_blocks * gpt.block_size))
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001169 return ('Nothing to expand for disk image %s partition %s.' %
1170 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001171
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001172 class Add(SubCommand):
1173 """Add, edit, or remove a partition entry.
1174
1175 Use the -i option to modify an existing partition.
1176 The -b, -s, and -t options must be given for new partitions.
1177
1178 The partition type may also be given as one of these aliases:
1179
1180 firmware ChromeOS firmware
1181 kernel ChromeOS kernel
1182 rootfs ChromeOS rootfs
1183 data Linux data
1184 reserved ChromeOS reserved
1185 efi EFI System Partition
1186 unused Unused (nonexistent) partition
1187 """
1188 def DefineArgs(self, parser):
1189 parser.add_argument(
1190 '-i', '--number', type=int,
1191 help='Specify partition (default is next available)')
1192 parser.add_argument(
1193 '-b', '--begin', type=int,
1194 help='Beginning sector')
1195 parser.add_argument(
1196 '-s', '--sectors', type=int,
1197 help='Size in sectors (logical blocks).')
1198 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001199 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001200 help='Partition Type GUID')
1201 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001202 '-u', '--unique-guid', type=GUID,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001203 help='Partition Unique ID')
1204 parser.add_argument(
1205 '-l', '--label',
1206 help='Label')
1207 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001208 '-S', '--successful', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001209 help='set Successful flag')
1210 parser.add_argument(
1211 '-T', '--tries', type=int,
1212 help='set Tries flag (0-15)')
1213 parser.add_argument(
1214 '-P', '--priority', type=int,
1215 help='set Priority flag (0-15)')
1216 parser.add_argument(
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001217 '-R', '--required', type=int, choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001218 help='set Required flag')
1219 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001220 '-B', '--boot-legacy', dest='legacy_boot', type=int,
Yilin Yangbf84d2e2020-05-13 10:34:46 +08001221 choices=list(range(2)),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001222 help='set Legacy Boot flag')
1223 parser.add_argument(
1224 '-A', '--attribute', dest='raw_16', type=int,
1225 help='set raw 16-bit attribute value (bits 48-63)')
1226 parser.add_argument(
1227 'image_file', type=argparse.FileType('rb+'),
1228 help='Disk image file to modify.')
1229
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001230 def Execute(self, args):
1231 gpt = GPT.LoadFromFile(args.image_file)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001232 number = args.number
1233 if number is None:
Hung-Te Linc5196682018-04-18 22:59:59 +08001234 number = next(p for p in gpt.partitions if p.IsUnused()).number
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001235
1236 # First and last LBA must be calculated explicitly because the given
1237 # argument is size.
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001238 part = gpt.GetPartition(number)
Hung-Te Linc5196682018-04-18 22:59:59 +08001239 is_new_part = part.IsUnused()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001240
1241 if is_new_part:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001242 part.Zero()
1243 part.Update(
Hung-Te Linc5196682018-04-18 22:59:59 +08001244 FirstLBA=gpt.GetMaxUsedLBA() + 1,
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001245 LastLBA=gpt.header.LastUsableLBA,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001246 UniqueGUID=GUID.Random(),
Hung-Te Linf641d302018-04-18 15:09:35 +08001247 TypeGUID=gpt.GetTypeGUID('data'))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001248
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001249 def UpdateAttr(name):
1250 value = getattr(args, name)
1251 if value is None:
1252 return
1253 setattr(attrs, name, value)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001254
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001255 def GetArg(arg_value, default_value):
1256 return default_value if arg_value is None else arg_value
1257
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001258 attrs = part.Attributes
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001259 for name in ['legacy_boot', 'required', 'priority', 'tries',
1260 'successful', 'raw_16']:
1261 UpdateAttr(name)
1262 first_lba = GetArg(args.begin, part.FirstLBA)
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001263 part.Update(
1264 Names=GetArg(args.label, part.Names),
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001265 FirstLBA=first_lba,
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001266 LastLBA=first_lba - 1 + GetArg(args.sectors, part.blocks),
1267 TypeGUID=GetArg(args.type_guid, part.TypeGUID),
1268 UniqueGUID=GetArg(args.unique_guid, part.UniqueGUID),
1269 Attributes=attrs)
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001270
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001271 # Wipe partition again if it should be empty.
1272 if part.IsUnused():
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001273 part.Zero()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001274
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001275 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001276 args.image_file.close()
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001277 if part.IsUnused():
1278 # If we do ('%s' % part) there will be TypeError.
Hung-Te Linbad46112018-05-15 16:39:14 +08001279 return 'Deleted (zeroed) %s.' % (part,)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001280 return ('%s %s (%s+%s).' %
1281 ('Added' if is_new_part else 'Modified',
1282 part, part.FirstLBA, part.blocks))
Hung-Te Linfcd1a8d2018-04-17 15:15:01 +08001283
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001284 class Show(SubCommand):
1285 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001286
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001287 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001288 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001289
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001290 def DefineArgs(self, parser):
1291 parser.add_argument(
1292 '--numeric', '-n', action='store_true',
1293 help='Numeric output only.')
1294 parser.add_argument(
1295 '--quick', '-q', action='store_true',
1296 help='Quick output.')
1297 parser.add_argument(
1298 '-i', '--number', type=int,
1299 help='Show specified partition only, with format args.')
1300 for name, help_str in GPTCommands.FORMAT_ARGS:
1301 # TODO(hungte) Alert if multiple args were specified.
1302 parser.add_argument(
1303 '--%s' % name, '-%c' % name[0], action='store_true',
1304 help='[format] %s.' % help_str)
1305 parser.add_argument(
1306 'image_file', type=argparse.FileType('rb'),
1307 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001308
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001309 def Execute(self, args):
1310 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001311
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001312 def FormatTypeGUID(p):
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001313 guid = p.TypeGUID
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001314 if not args.numeric:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001315 names = gpt.TYPE_GUID_MAP.get(guid)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001316 if names:
1317 return names
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001318 return str(guid)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001319
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001320 def IsBootableType(guid):
1321 if not guid:
1322 return False
1323 return guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001324
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001325 def FormatAttribute(attrs, chromeos_kernel=False):
1326 if args.numeric:
1327 return '[%x]' % (attrs.raw >> 48)
1328 results = []
1329 if chromeos_kernel:
1330 results += [
1331 'priority=%d' % attrs.priority,
1332 'tries=%d' % attrs.tries,
1333 'successful=%d' % attrs.successful]
1334 if attrs.required:
1335 results += ['required=1']
1336 if attrs.legacy_boot:
1337 results += ['legacy_boot=1']
1338 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001339
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001340 def ApplyFormatArgs(p):
1341 if args.begin:
1342 return p.FirstLBA
Fei Shao12ecf382020-06-23 18:32:26 +08001343 if args.size:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001344 return p.blocks
Fei Shao12ecf382020-06-23 18:32:26 +08001345 if args.type:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001346 return FormatTypeGUID(p)
Fei Shao12ecf382020-06-23 18:32:26 +08001347 if args.unique:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001348 return p.UniqueGUID
Fei Shao12ecf382020-06-23 18:32:26 +08001349 if args.label:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001350 return p.Names
Fei Shao12ecf382020-06-23 18:32:26 +08001351 if args.Successful:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001352 return p.Attributes.successful
Fei Shao12ecf382020-06-23 18:32:26 +08001353 if args.Priority:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001354 return p.Attributes.priority
Fei Shao12ecf382020-06-23 18:32:26 +08001355 if args.Tries:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001356 return p.Attributes.tries
Fei Shao12ecf382020-06-23 18:32:26 +08001357 if args.Legacy:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001358 return p.Attributes.legacy_boot
Fei Shao12ecf382020-06-23 18:32:26 +08001359 if args.Attribute:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001360 return '[%x]' % (p.Attributes.raw >> 48)
Yilin Yang15a3f8f2020-01-03 17:49:00 +08001361 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001362
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001363 def IsFormatArgsSpecified():
1364 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001365
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001366 gpt = GPT.LoadFromFile(args.image_file)
1367 logging.debug('%r', gpt.header)
1368 fmt = '%12s %11s %7s %s'
1369 fmt2 = '%32s %s: %s'
1370 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001371
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001372 if IsFormatArgsSpecified() and args.number is None:
1373 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001374
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001375 if not (args.number is None or
1376 0 < args.number <= gpt.header.PartitionEntriesNumber):
1377 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001378
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001379 partitions = gpt.partitions
1380 do_print_gpt_blocks = False
1381 if not (args.quick or IsFormatArgsSpecified()):
1382 print(fmt % header)
1383 if args.number is None:
1384 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001385
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001386 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001387 if gpt.pmbr:
1388 print(fmt % (0, 1, '', 'PMBR'))
1389 if gpt.is_secondary:
1390 print(fmt % (gpt.header.BackupLBA, 1, 'IGNORED', 'Pri GPT header'))
1391 else:
1392 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
1393 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
1394 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001395
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001396 for p in partitions:
1397 if args.number is None:
1398 # Skip unused partitions.
1399 if p.IsUnused():
1400 continue
1401 elif p.number != args.number:
1402 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001403
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001404 if IsFormatArgsSpecified():
1405 print(ApplyFormatArgs(p))
1406 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001407
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001408 print(fmt % (p.FirstLBA, p.blocks, p.number,
1409 FormatTypeGUID(p) if args.quick else
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001410 'Label: "%s"' % p.Names))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001411
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001412 if not args.quick:
1413 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001414 print(fmt2 % ('', 'UUID', p.UniqueGUID))
1415 if args.numeric or IsBootableType(p.TypeGUID):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001416 print(fmt2 % ('', 'Attr', FormatAttribute(
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001417 p.Attributes, p.IsChromeOSKernel())))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001418
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001419 if do_print_gpt_blocks:
Hung-Te Linc34d89c2018-04-17 15:11:34 +08001420 if gpt.is_secondary:
1421 header = gpt.header
1422 else:
1423 f = args.image_file
1424 f.seek(gpt.header.BackupLBA * gpt.block_size)
1425 header = gpt.Header.ReadFrom(f)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001426 print(fmt % (header.PartitionEntriesStartingLBA,
1427 gpt.GetPartitionTableBlocks(header), '',
1428 'Sec GPT table'))
1429 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001430
Hung-Te Lin3b491672018-04-19 01:41:20 +08001431 # Check integrity after showing all fields.
1432 gpt.CheckIntegrity()
1433
Hung-Te Linfe724f82018-04-18 15:03:58 +08001434 class Prioritize(SubCommand):
1435 """Reorder the priority of all kernel partitions.
1436
1437 Reorder the priority of all active ChromeOS Kernel partitions.
1438
1439 With no options this will set the lowest active kernel to priority 1 while
1440 maintaining the original order.
1441 """
1442
1443 def DefineArgs(self, parser):
1444 parser.add_argument(
1445 '-P', '--priority', type=int,
1446 help=('Highest priority to use in the new ordering. '
1447 'The other partitions will be ranked in decreasing '
1448 'priority while preserving their original order. '
1449 'If necessary the lowest ranks will be coalesced. '
1450 'No active kernels will be lowered to priority 0.'))
1451 parser.add_argument(
1452 '-i', '--number', type=int,
1453 help='Specify the partition to make the highest in the new order.')
1454 parser.add_argument(
1455 '-f', '--friends', action='store_true',
1456 help=('Friends of the given partition (those with the same '
1457 'starting priority) are also updated to the new '
1458 'highest priority. '))
1459 parser.add_argument(
1460 'image_file', type=argparse.FileType('rb+'),
1461 help='Disk image file to prioritize.')
1462
1463 def Execute(self, args):
1464 gpt = GPT.LoadFromFile(args.image_file)
1465 parts = [p for p in gpt.partitions if p.IsChromeOSKernel()]
Hung-Te Lin138389f2018-05-15 17:55:00 +08001466 parts.sort(key=lambda p: p.Attributes.priority, reverse=True)
Fei Shao95075802020-06-16 16:55:25 +08001467 groups = {k: list(g) for k, g in itertools.groupby(
1468 parts, lambda p: p.Attributes.priority)}
Hung-Te Linfe724f82018-04-18 15:03:58 +08001469 if args.number:
Hung-Te Lin5f0dea42018-04-18 23:20:11 +08001470 p = gpt.GetPartition(args.number)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001471 if p not in parts:
1472 raise GPTError('%s is not a ChromeOS kernel.' % p)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001473 pri = p.Attributes.priority
1474 friends = groups.pop(pri)
1475 new_pri = max(groups) + 1
Hung-Te Linfe724f82018-04-18 15:03:58 +08001476 if args.friends:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001477 groups[new_pri] = friends
Hung-Te Linfe724f82018-04-18 15:03:58 +08001478 else:
Hung-Te Lin138389f2018-05-15 17:55:00 +08001479 groups[new_pri] = [p]
1480 friends.remove(p)
1481 if friends:
1482 groups[pri] = friends
1483
1484 if 0 in groups:
1485 # Do not change any partitions with priority=0
1486 groups.pop(0)
1487
Yilin Yang78fa12e2019-09-25 14:21:10 +08001488 prios = list(groups)
Hung-Te Lin138389f2018-05-15 17:55:00 +08001489 prios.sort(reverse=True)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001490
1491 # Max priority is 0xf.
1492 highest = min(args.priority or len(prios), 0xf)
1493 logging.info('New highest priority: %s', highest)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001494
Hung-Te Lin138389f2018-05-15 17:55:00 +08001495 for i, pri in enumerate(prios):
1496 new_priority = max(1, highest - i)
1497 for p in groups[pri]:
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001498 attrs = p.Attributes
Hung-Te Linfe724f82018-04-18 15:03:58 +08001499 old_priority = attrs.priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001500 if old_priority == new_priority:
1501 continue
Hung-Te Linfe724f82018-04-18 15:03:58 +08001502 attrs.priority = new_priority
Hung-Te Lin138389f2018-05-15 17:55:00 +08001503 if attrs.tries < 1 and not attrs.successful:
1504 attrs.tries = 15 # Max tries for new active partition.
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001505 p.Update(Attributes=attrs)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001506 logging.info('%s priority changed from %s to %s.', p, old_priority,
1507 new_priority)
Hung-Te Linfe724f82018-04-18 15:03:58 +08001508
1509 gpt.WriteToFile(args.image_file)
Yilin Yangf95c25a2019-12-23 15:38:51 +08001510 args.image_file.close()
Hung-Te Linfe724f82018-04-18 15:03:58 +08001511
Hung-Te Linf641d302018-04-18 15:09:35 +08001512 class Find(SubCommand):
1513 """Locate a partition by its GUID.
1514
1515 Find a partition by its UUID or label. With no specified DRIVE it scans all
1516 physical drives.
1517
1518 The partition type may also be given as one of these aliases:
1519
1520 firmware ChromeOS firmware
1521 kernel ChromeOS kernel
1522 rootfs ChromeOS rootfs
1523 data Linux data
1524 reserved ChromeOS reserved
1525 efi EFI System Partition
1526 unused Unused (nonexistent) partition
1527 """
1528 def DefineArgs(self, parser):
1529 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001530 '-t', '--type-guid', type=GPT.GetTypeGUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001531 help='Search for Partition Type GUID')
1532 parser.add_argument(
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001533 '-u', '--unique-guid', type=GUID,
Hung-Te Linf641d302018-04-18 15:09:35 +08001534 help='Search for Partition Unique GUID')
1535 parser.add_argument(
1536 '-l', '--label',
1537 help='Search for Label')
1538 parser.add_argument(
1539 '-n', '--numeric', action='store_true',
1540 help='Numeric output only.')
1541 parser.add_argument(
1542 '-1', '--single-match', action='store_true',
1543 help='Fail if more than one match is found.')
1544 parser.add_argument(
1545 '-M', '--match-file', type=str,
1546 help='Matching partition data must also contain MATCH_FILE content.')
1547 parser.add_argument(
1548 '-O', '--offset', type=int, default=0,
1549 help='Byte offset into partition to match content (default 0).')
1550 parser.add_argument(
1551 'drive', type=argparse.FileType('rb+'), nargs='?',
1552 help='Drive or disk image file to find.')
1553
1554 def Execute(self, args):
1555 if not any((args.type_guid, args.unique_guid, args.label)):
1556 raise GPTError('You must specify at least one of -t, -u, or -l')
1557
1558 drives = [args.drive.name] if args.drive else (
1559 '/dev/%s' % name for name in subprocess.check_output(
Yilin Yang42ba5c62020-05-05 10:32:34 +08001560 'lsblk -d -n -r -o name', shell=True, encoding='utf-8').split())
Hung-Te Linf641d302018-04-18 15:09:35 +08001561
1562 match_pattern = None
1563 if args.match_file:
1564 with open(args.match_file) as f:
1565 match_pattern = f.read()
1566
1567 found = 0
1568 for drive in drives:
1569 try:
1570 gpt = GPT.LoadFromFile(drive)
1571 except GPTError:
1572 if args.drive:
1573 raise
1574 # When scanning all block devices on system, ignore failure.
1575
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001576 def Unmatch(a, b):
1577 return a is not None and a != b
1578
Hung-Te Linf641d302018-04-18 15:09:35 +08001579 for p in gpt.partitions:
Hung-Te Linbf8aa272018-04-19 03:02:29 +08001580 if (p.IsUnused() or
Hung-Te Lin86ca4bb2018-04-25 10:22:10 +08001581 Unmatch(args.label, p.Names) or
1582 Unmatch(args.unique_guid, p.UniqueGUID) or
1583 Unmatch(args.type_guid, p.TypeGUID)):
Hung-Te Linf641d302018-04-18 15:09:35 +08001584 continue
1585 if match_pattern:
1586 with open(drive, 'rb') as f:
1587 f.seek(p.offset + args.offset)
1588 if f.read(len(match_pattern)) != match_pattern:
1589 continue
1590 # Found the partition, now print.
1591 found += 1
1592 if args.numeric:
1593 print(p.number)
1594 else:
1595 # This is actually more for block devices.
1596 print('%s%s%s' % (p.image, 'p' if p.image[-1].isdigit() else '',
1597 p.number))
1598
1599 if found < 1 or (args.single_match and found > 1):
1600 return 1
1601 return 0
1602
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001603
1604def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001605 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001606 parser = argparse.ArgumentParser(description='GPT Utility.')
1607 parser.add_argument('--verbose', '-v', action='count', default=0,
1608 help='increase verbosity.')
1609 parser.add_argument('--debug', '-d', action='store_true',
1610 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +08001611 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001612
1613 args = parser.parse_args()
1614 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
1615 if args.debug:
1616 log_level = logging.DEBUG
1617 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
1618 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001619 try:
Hung-Te Linf641d302018-04-18 15:09:35 +08001620 code = commands.Execute(args)
Peter Shih533566a2018-09-05 17:48:03 +08001621 if isinstance(code, int):
Hung-Te Linf641d302018-04-18 15:09:35 +08001622 sys.exit(code)
Yilin Yang0724c9d2019-11-15 15:53:45 +08001623 elif isinstance(code, str):
Hung-Te Linbad46112018-05-15 16:39:14 +08001624 print('OK: %s' % code)
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001625 except Exception as e:
1626 if args.verbose or args.debug:
1627 logging.exception('Failure in command [%s]', args.command)
Fei Shao9cd93ea2020-06-16 18:31:22 +08001628 sys.exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +08001629
1630
1631if __name__ == '__main__':
1632 main()