blob: 3fe2d3ee2248d4a57ad4829ad5661cfe5845c7a4 [file] [log] [blame]
You-Cheng Syud5692942018-01-04 14:40:59 +08001#!/usr/bin/env python
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
33import collections
34import logging
35import os
36import struct
37import uuid
38
39
40# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
41# declared as 'signed' (l) instead of 'unsigned' (L).
42# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080043HEADER_DESCRIPTION = """
Hung-Te Linc772e1a2017-04-14 16:50:50 +080044 8s Signature
45 4s Revision
46 L HeaderSize
47 l CRC32
48 4s Reserved
49 Q CurrentLBA
50 Q BackupLBA
51 Q FirstUsableLBA
52 Q LastUsableLBA
53 16s DiskGUID
54 Q PartitionEntriesStartingLBA
55 L PartitionEntriesNumber
56 L PartitionEntrySize
57 l PartitionArrayCRC32
58"""
59
60# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080061PARTITION_DESCRIPTION = """
Hung-Te Linc772e1a2017-04-14 16:50:50 +080062 16s TypeGUID
63 16s UniqueGUID
64 Q FirstLBA
65 Q LastLBA
66 Q Attributes
67 72s Names
68"""
69
70
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080071def BitProperty(getter, setter, shift, mask):
72 """A generator for bit-field properties.
73
74 This is used inside a class to manipulate an integer-like variable using
75 properties. The getter and setter should be member functions to change the
76 underlying member data.
Hung-Te Linc772e1a2017-04-14 16:50:50 +080077
78 Args:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080079 getter: a function to read integer type variable (for all the bits).
80 setter: a function to set the new changed integer type variable.
81 shift: integer for how many bits should be shifted (right).
82 mask: integer for the mask to filter out bit field.
Hung-Te Linc772e1a2017-04-14 16:50:50 +080083 """
Hung-Te Lin49ac3c22018-04-17 14:37:54 +080084 def _getter(self):
85 return (getter(self) >> shift) & mask
86 def _setter(self, value):
87 assert value & mask == value, (
88 'Value %s out of range (mask=%s)' % (value, mask))
89 setter(self, getter(self) & ~(mask << shift) | value << shift)
90 return property(_getter, _setter)
91
92
93class GPTBlob(object):
94 """A decorator class to help accessing GPT blobs as named tuple.
95
96 To use this, specify the blob description (struct format and named tuple field
97 names) above the derived class, for example:
98
99 @GPTBlob(description):
100 class Header(GPTObject):
101 pass
102 """
103 def __init__(self, description):
104 spec = description.split()
105 self.struct_format = '<' + ''.join(spec[::2])
106 self.fields = spec[1::2]
107
108 def __call__(self, cls):
109 new_bases = ((
110 collections.namedtuple(cls.__name__, self.fields),) + cls.__bases__)
111 new_cls = type(cls.__name__, new_bases, dict(cls.__dict__))
112 setattr(new_cls, 'FORMAT', self.struct_format)
113 return new_cls
114
115
116class GPTObject(object):
117 """An object in GUID Partition Table.
118
119 This needs to be decorated by @GPTBlob(description) and inherited by a real
120 class. Properties (not member functions) in CamelCase should be reserved for
121 named tuple attributes.
122
123 To create a new object, use class method ReadFrom(), which takes a stream
124 as input or None to create with all elements set to zero. To make changes to
125 named tuple elements, use member function Clone(changes).
126
127 It is also possible to attach some additional properties to the object as meta
128 data (for example path of the underlying image file). To do that, specify the
129 data as keyword arguments when calling ReadFrom(). These properties will be
130 preserved when you call Clone().
131
132 A special case is "reset named tuple elements of an object but keeping all
133 properties", for example changing a partition object to unused (zeroed).
134 ReadFrom() is a class method so properties won't be copied. You need to
135 call as cls.ReadFrom(None, **p.__dict__), or a short cut - p.CloneAndZero().
136 """
137
138 FORMAT = None
139 """The struct.{pack,unpack} format string, and should be set by GPTBlob."""
140
141 CLONE_CONVERTERS = None
142 """A dict (name, cvt) to convert input arguments into named tuple data.
143
144 `name` is a string for the name of argument to convert.
145 `cvt` is a callable to convert value. The return value may be:
146 - a tuple in (new_name, value): save the value as new name.
147 - otherwise, save the value in original name.
148 Note tuple is an invalid input for struct.unpack so it's used for the
149 special value.
150 """
151
152 @classmethod
153 def ReadFrom(cls, f, **kargs):
154 """Reads and decode an object from stream.
155
156 Args:
157 f: a stream to read blob, or None to decode with all zero bytes.
158 kargs: a dict for additional attributes in object.
159 """
160 if f is None:
161 reader = lambda num: '\x00' * num
162 else:
163 reader = f.read
164 data = cls(*struct.unpack(cls.FORMAT, reader(struct.calcsize(cls.FORMAT))))
165 # Named tuples do not accept kargs in constructor.
166 data.__dict__.update(kargs)
167 return data
168
169 def Clone(self, **dargs):
170 """Clones a new instance with modifications.
171
172 GPT objects are usually named tuples that are immutable, so the only way
173 to make changes is to create a new instance with modifications.
174
175 Args:
176 dargs: a dict with all modifications.
177 """
178 for name, convert in (self.CLONE_CONVERTERS or {}).iteritems():
179 if name not in dargs:
180 continue
181 result = convert(dargs.pop(name))
182 if isinstance(result, tuple):
183 assert len(result) == 2, 'Converted tuple must be (name, value).'
184 dargs[result[0]] = result[1]
185 else:
186 dargs[name] = result
187
188 cloned = self._replace(**dargs)
189 cloned.__dict__.update(self.__dict__)
190 return cloned
191
192 def CloneAndZero(self, **dargs):
193 """Short cut to create a zeroed object while keeping all properties.
194
195 This is very similar to Clone except all named tuple elements will be zero.
196 Also different from class method ReadFrom(None) because this keeps all
197 properties from one object.
198 """
199 cloned = self.ReadFrom(None, **self.__dict__)
200 return cloned.Clone(**dargs) if dargs else cloned
201
202 @property
203 def blob(self):
204 """Returns the object in formatted bytes."""
205 return struct.pack(self.FORMAT, *self)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800206
207
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800208class GPTError(Exception):
209 """All exceptions by GPT."""
210 pass
211
212
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800213class GPT(object):
214 """A GPT helper class.
215
216 To load GPT from an existing disk image file, use `LoadFromFile`.
217 After modifications were made, use `WriteToFile` to commit changes.
218
219 Attributes:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800220 header: a namedtuple of GPT header.
221 partitions: a list of GPT partition entry nametuple.
222 block_size: integer for size of bytes in one block (sector).
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800223 """
224
Hung-Te Linf148d322018-04-13 10:24:42 +0800225 DEFAULT_BLOCK_SIZE = 512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800226 TYPE_GUID_UNUSED = '\x00' * 16
227 TYPE_GUID_MAP = {
228 '00000000-0000-0000-0000-000000000000': 'Unused',
229 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7': 'Linux data',
230 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309': 'ChromeOS kernel',
231 '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC': 'ChromeOS rootfs',
232 '2E0A753D-9E48-43B0-8337-B15192CB1B5E': 'ChromeOS reserved',
233 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3': 'ChromeOS firmware',
234 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': 'EFI System Partition',
235 }
236 TYPE_GUID_LIST_BOOTABLE = [
237 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309', # ChromeOS kernel
238 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B', # EFI System Partition
239 ]
240
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800241 @GPTBlob(HEADER_DESCRIPTION)
242 class Header(GPTObject):
243 """Wrapper to Header in GPT."""
244 SIGNATURES = ['EFI PART', 'CHROMEOS']
245 SIGNATURE_IGNORE = 'IGNOREME'
246 DEFAULT_REVISION = '\x00\x00\x01\x00'
247
248 DEFAULT_PARTITION_ENTRIES = 128
249 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
250
251 def Clone(self, **dargs):
252 """Creates a new instance with modifications.
253
254 GPT objects are usually named tuples that are immutable, so the only way
255 to make changes is to create a new instance with modifications.
256
257 CRC32 is always updated but PartitionArrayCRC32 must be updated explicitly
258 since we can't track changes in GPT.partitions automatically.
259
260 Note since GPTHeader.Clone will always update CRC, we can only check and
261 compute CRC by super(GPT.Header, header).Clone, or header._replace.
262 """
263 dargs['CRC32'] = 0
264 header = super(GPT.Header, self).Clone(**dargs)
265 return super(GPT.Header, header).Clone(CRC32=binascii.crc32(header.blob))
266
267 class PartitionAttributes(object):
268 """Wrapper for Partition.Attributes.
269
270 This can be created using Partition.attrs, but the changed properties won't
271 apply to underlying Partition until an explicit call with
272 Partition.Clone(Attributes=new_attrs).
273 """
274
275 def __init__(self, attrs):
276 self._attrs = attrs
277
278 @property
279 def raw(self):
280 """Returns the raw integer type attributes."""
281 return self._Get()
282
283 def _Get(self):
284 return self._attrs
285
286 def _Set(self, value):
287 self._attrs = value
288
289 successful = BitProperty(_Get, _Set, 56, 1)
290 tries = BitProperty(_Get, _Set, 52, 0xf)
291 priority = BitProperty(_Get, _Set, 48, 0xf)
292 legacy_boot = BitProperty(_Get, _Set, 2, 1)
293 required = BitProperty(_Get, _Set, 0, 1)
294
295 @GPTBlob(PARTITION_DESCRIPTION)
296 class Partition(GPTObject):
297 """The partition entry in GPT.
298
299 Please include following properties when creating a Partition object:
300 - image: a string for path to the image file the partition maps to.
301 - number: the 1-based partition number.
302 - block_size: an integer for size of each block (LBA, or sector).
303 """
304 NAMES_ENCODING = 'utf-16-le'
305 NAMES_LENGTH = 72
306
307 CLONE_CONVERTERS = {
308 # TODO(hungte) check if encoded name is too long.
309 'label': lambda l: (None if l is None else
310 ('Names', l.encode(GPT.Partition.NAMES_ENCODING))),
311 'TypeGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
312 'UniqueGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
313 'Attributes': (
314 lambda v: v.raw if isinstance(v, GPT.PartitionAttributes) else v),
315 }
316
317 def __str__(self):
318 return '%s#%s' % (self.image, self.number)
319
320 def IsUnused(self):
321 """Returns if the partition is unused and can be allocated."""
322 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
323
324 @property
325 def blocks(self):
326 """Return size of partition in blocks (see block_size)."""
327 return self.LastLBA - self.FirstLBA + 1
328
329 @property
330 def offset(self):
331 """Returns offset to partition in bytes."""
332 return self.FirstLBA * self.block_size
333
334 @property
335 def size(self):
336 """Returns size of partition in bytes."""
337 return self.blocks * self.block_size
338
339 @property
340 def type_guid(self):
341 return uuid.UUID(bytes_le=self.TypeGUID)
342
343 @property
344 def unique_guid(self):
345 return uuid.UUID(bytes_le=self.UniqueGUID)
346
347 @property
348 def label(self):
349 """Returns the Names in decoded string type."""
350 return self.Names.decode(self.NAMES_ENCODING).strip('\0')
351
352 @property
353 def attrs(self):
354 return GPT.PartitionAttributes(self.Attributes)
355
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800356 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800357 """GPT constructor.
358
359 See LoadFromFile for how it's usually used.
360 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800361 self.header = None
362 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800363 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800364
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800365 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800366 def LoadFromFile(cls, image):
367 """Loads a GPT table from give disk image file object.
368
369 Args:
370 image: a string as file path or a file-like object to read from.
371 """
372 if isinstance(image, basestring):
373 with open(image, 'rb') as f:
374 return cls.LoadFromFile(f)
375
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800376 gpt = cls()
Hung-Te Linf148d322018-04-13 10:24:42 +0800377 # Try DEFAULT_BLOCK_SIZE, then 4K.
378 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800379 image.seek(block_size * 1)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800380 header = gpt.Header.ReadFrom(image)
381 if header.Signature in cls.Header.SIGNATURES:
Hung-Te Linf148d322018-04-13 10:24:42 +0800382 gpt.block_size = block_size
383 break
384 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800385 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800386
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800387 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800388 def ReadPartition(image, i):
389 p = gpt.Partition.ReadFrom(
390 image, image=image.name, number=i + 1, block_size=gpt.block_size)
391 return p
392
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800393 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800394 gpt.partitions = [
395 ReadPartition(image, i) for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800396 return gpt
397
398 def GetValidPartitions(self):
399 """Returns the list of partitions before entry with empty type GUID.
400
401 In partition table, the first entry with empty type GUID indicates end of
402 valid partitions. In most implementations all partitions after that should
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800403 be zeroed. However, few implementations for example cgpt, may create
404 partitions in arbitrary order so use this carefully.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800405 """
406 for i, p in enumerate(self.partitions):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800407 if p.IsUnused():
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800408 return self.partitions[:i]
409 return self.partitions
410
411 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800412 """Returns the max LastLBA from all used partitions."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800413 parts = [p for p in self.partitions if not p.IsUnused()]
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800414 return (max(p.LastLBA for p in parts)
415 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800416
417 def GetPartitionTableBlocks(self, header=None):
418 """Returns the blocks (or LBA) of partition table from given header."""
419 if header is None:
420 header = self.header
421 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800422 blocks = size / self.block_size
423 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800424 blocks += 1
425 return blocks
426
427 def Resize(self, new_size):
428 """Adjust GPT for a disk image in given size.
429
430 Args:
431 new_size: Integer for new size of disk image file.
432 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800433 old_size = self.block_size * (self.header.BackupLBA + 1)
434 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800435 raise GPTError(
436 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800437 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800438 if old_size != new_size:
439 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800440 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800441 else:
442 logging.info('Image size (%d, LBA=%d) not changed.',
443 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800444 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800445
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800446 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800447 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800448 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800449
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800450 if last_usable_lba < self.header.LastUsableLBA:
451 max_used_lba = self.GetMaxUsedLBA()
452 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800453 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800454
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800455 self.header = self.header.Clone(
456 BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800457
458 def GetFreeSpace(self):
459 """Returns the free (available) space left according to LastUsableLBA."""
460 max_lba = self.GetMaxUsedLBA()
461 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800462 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800463
464 def ExpandPartition(self, i):
465 """Expands a given partition to last usable LBA.
466
467 Args:
468 i: Index (0-based) of target partition.
469 """
470 # Assume no partitions overlap, we need to make sure partition[i] has
471 # largest LBA.
472 if i < 0 or i >= len(self.GetValidPartitions()):
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800473 raise GPTError('Partition index %d is invalid.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800474 p = self.partitions[i]
475 max_used_lba = self.GetMaxUsedLBA()
476 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800477 raise GPTError(
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800478 'Cannot expand partition %d because it is not the last allocated '
479 'partition.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800480
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800481 old_blocks = p.blocks
482 p = p.Clone(LastLBA=self.header.LastUsableLBA)
483 new_blocks = p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800484 self.partitions[i] = p
485 logging.warn('Partition NR=%d expanded, size in LBA: %d -> %d.',
486 i + 1, old_blocks, new_blocks)
487
488 def UpdateChecksum(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800489 """Updates all checksum fields in GPT objects.
490
491 The Header.CRC32 is automatically updated in Header.Clone().
492 """
493 parts = ''.join(p.blob for p in self.partitions)
494 self.header = self.header.Clone(
495 PartitionArrayCRC32=binascii.crc32(parts))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800496
497 def GetBackupHeader(self):
498 """Returns the backup header according to current header."""
499 partitions_starting_lba = (
500 self.header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800501 return self.header.Clone(
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800502 BackupLBA=self.header.CurrentLBA,
503 CurrentLBA=self.header.BackupLBA,
504 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800505
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800506 def WriteToFile(self, image):
507 """Updates partition table in a disk image file.
508
509 Args:
510 image: a string as file path or a file-like object to write into.
511 """
512 if isinstance(image, basestring):
513 with open(image, 'rb+') as f:
514 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800515
516 def WriteData(name, blob, lba):
517 """Writes a blob into given location."""
518 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800519 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800520 image.seek(lba * self.block_size)
521 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800522
523 self.UpdateChecksum()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800524 parts_blob = ''.join(p.blob for p in self.partitions)
525 WriteData('GPT Header', self.header.blob, self.header.CurrentLBA)
526 WriteData(
527 'GPT Partitions', parts_blob, self.header.PartitionEntriesStartingLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800528 logging.info('Usable LBA: First=%d, Last=%d',
529 self.header.FirstUsableLBA, self.header.LastUsableLBA)
530 backup_header = self.GetBackupHeader()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800531 WriteData(
532 'Backup Partitions', parts_blob,
533 backup_header.PartitionEntriesStartingLBA)
534 WriteData('Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800535
536
537class GPTCommands(object):
538 """Collection of GPT sub commands for command line to use.
539
540 The commands are derived from `cgpt`, but not necessary to be 100% compatible
541 with cgpt.
542 """
543
544 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800545 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800546 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800547 ('type', 'type guid'),
548 ('unique', 'unique guid'),
549 ('label', 'label'),
550 ('Successful', 'Successful flag'),
551 ('Tries', 'Tries flag'),
552 ('Priority', 'Priority flag'),
553 ('Legacy', 'Legacy Boot flag'),
554 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800555
556 def __init__(self):
557 pass
558
559 @classmethod
560 def RegisterRepair(cls, p):
561 """Registers the repair command to argparser.
562
563 Args:
564 p: An argparse parser instance.
565 """
566 p.add_argument(
567 '--expand', action='store_true', default=False,
568 help='Expands stateful partition to full disk.')
569 p.add_argument('image_file', type=argparse.FileType('rb+'),
570 help='Disk image file to repair.')
571
572 def Repair(self, args):
573 """Repair damaged GPT headers and tables."""
574 gpt = GPT.LoadFromFile(args.image_file)
575 gpt.Resize(os.path.getsize(args.image_file.name))
576
577 free_space = gpt.GetFreeSpace()
578 if args.expand:
579 if free_space:
580 gpt.ExpandPartition(0)
581 else:
582 logging.warn('- No extra space to expand.')
583 elif free_space:
584 logging.warn('Extra space found (%d, LBA=%d), '
Peter Shihc7156ca2018-02-26 14:46:24 +0800585 'use --expand to expand partitions.',
Hung-Te Linf148d322018-04-13 10:24:42 +0800586 free_space, free_space / gpt.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800587
588 gpt.WriteToFile(args.image_file)
589 print('Disk image file %s repaired.' % args.image_file.name)
590
591 @classmethod
592 def RegisterShow(cls, p):
593 """Registers the repair command to argparser.
594
595 Args:
596 p: An argparse parser instance.
597 """
598 p.add_argument('--numeric', '-n', action='store_true',
599 help='Numeric output only.')
600 p.add_argument('--quick', '-q', action='store_true',
601 help='Quick output.')
602 p.add_argument('--index', '-i', type=int, default=None,
603 help='Show specified partition only, with format args.')
604 for name, help_str in cls.FORMAT_ARGS:
605 # TODO(hungte) Alert if multiple args were specified.
606 p.add_argument('--%s' % name, '-%c' % name[0], action='store_true',
607 help='[format] %s.' % help_str)
608 p.add_argument('image_file', type=argparse.FileType('rb'),
609 help='Disk image file to show.')
610
611
612 def Show(self, args):
613 """Show partition table and entries."""
614
615 def FormatGUID(bytes_le):
616 return str(uuid.UUID(bytes_le=bytes_le)).upper()
617
618 def FormatTypeGUID(p):
619 guid_str = FormatGUID(p.TypeGUID)
620 if not args.numeric:
621 names = gpt.TYPE_GUID_MAP.get(guid_str)
622 if names:
623 return names
624 return guid_str
625
626 def FormatNames(p):
627 return p.Names.decode('utf-16-le').strip('\0')
628
629 def IsBootableType(type_guid):
630 return type_guid in gpt.TYPE_GUID_LIST_BOOTABLE
631
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800632 def FormatAttribute(attrs):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800633 if args.numeric:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800634 return '[%x]' % (attrs.raw >> 48)
635 if attrs.legacy_boot:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800636 return 'legacy_boot=1'
637 return 'priority=%d tries=%d successful=%d' % (
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800638 attrs.priority, attrs.tries, attrs.successful)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800639
640 def ApplyFormatArgs(p):
641 if args.begin:
642 return p.FirstLBA
643 elif args.size:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800644 return p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800645 elif args.type:
646 return FormatTypeGUID(p)
647 elif args.unique:
648 return FormatGUID(p.UniqueGUID)
649 elif args.label:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800650 return p.label
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800651 elif args.Successful:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800652 return p.attrs.successful
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800653 elif args.Priority:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800654 return p.attrs.priority
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800655 elif args.Tries:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800656 return p.attrs.tries
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800657 elif args.Legacy:
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800658 return p.attrs.legacy_boot
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800659 elif args.Attribute:
660 return '[%x]' % (p.Attributes >> 48)
661 else:
662 return None
663
664 def IsFormatArgsSpecified():
665 return any(getattr(args, arg[0]) for arg in self.FORMAT_ARGS)
666
667 gpt = GPT.LoadFromFile(args.image_file)
668 fmt = '%12s %11s %7s %s'
669 fmt2 = '%32s %s: %s'
670 header = ('start', 'size', 'part', 'contents')
671
672 if IsFormatArgsSpecified() and args.index is None:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800673 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800674
675 partitions = gpt.GetValidPartitions()
676 if not (args.index is None or 0 < args.index <= len(partitions)):
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800677 raise GPTError('Invalid partition index: %d' % args.index)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800678
679 do_print_gpt_blocks = False
680 if not (args.quick or IsFormatArgsSpecified()):
681 print(fmt % header)
682 if args.index is None:
683 do_print_gpt_blocks = True
684
685 if do_print_gpt_blocks:
686 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
687 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
688 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
689
690 for i, p in enumerate(partitions):
691 if args.index is not None and i != args.index - 1:
692 continue
693
694 if IsFormatArgsSpecified():
695 print(ApplyFormatArgs(p))
696 continue
697
698 type_guid = FormatGUID(p.TypeGUID)
699 print(fmt % (p.FirstLBA, p.LastLBA - p.FirstLBA + 1, i + 1,
700 FormatTypeGUID(p) if args.quick else
701 'Label: "%s"' % FormatNames(p)))
702
703 if not args.quick:
704 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
705 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
706 if args.numeric or IsBootableType(type_guid):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800707 print(fmt2 % ('', 'Attr', FormatAttribute(p.attrs)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800708
709 if do_print_gpt_blocks:
710 f = args.image_file
Hung-Te Linf148d322018-04-13 10:24:42 +0800711 f.seek(gpt.header.BackupLBA * gpt.block_size)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800712 backup_header = gpt.Header.ReadFrom(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800713 print(fmt % (backup_header.PartitionEntriesStartingLBA,
714 gpt.GetPartitionTableBlocks(backup_header), '',
715 'Sec GPT table'))
716 print(fmt % (gpt.header.BackupLBA, 1, '', 'Sec GPT header'))
717
718 def Create(self, args):
719 """Create or reset GPT headers and tables."""
720 del args # Not used yet.
721 raise NotImplementedError
722
723 def Add(self, args):
724 """Add, edit or remove a partition entry."""
725 del args # Not used yet.
726 raise NotImplementedError
727
728 def Boot(self, args):
729 """Edit the PMBR sector for legacy BIOSes."""
730 del args # Not used yet.
731 raise NotImplementedError
732
733 def Find(self, args):
734 """Locate a partition by its GUID."""
735 del args # Not used yet.
736 raise NotImplementedError
737
738 def Prioritize(self, args):
739 """Reorder the priority of all kernel partitions."""
740 del args # Not used yet.
741 raise NotImplementedError
742
743 def Legacy(self, args):
744 """Switch between GPT and Legacy GPT."""
745 del args # Not used yet.
746 raise NotImplementedError
747
748 @classmethod
749 def RegisterAllCommands(cls, subparsers):
750 """Registers all available commands to an argparser subparsers instance."""
751 subcommands = [('show', cls.Show, cls.RegisterShow),
752 ('repair', cls.Repair, cls.RegisterRepair)]
753 for name, invocation, register_command in subcommands:
754 register_command(subparsers.add_parser(name, help=invocation.__doc__))
755
756
757def main():
758 parser = argparse.ArgumentParser(description='GPT Utility.')
759 parser.add_argument('--verbose', '-v', action='count', default=0,
760 help='increase verbosity.')
761 parser.add_argument('--debug', '-d', action='store_true',
762 help='enable debug output.')
763 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
764 GPTCommands.RegisterAllCommands(subparsers)
765
766 args = parser.parse_args()
767 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
768 if args.debug:
769 log_level = logging.DEBUG
770 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
771 level=log_level)
772 commands = GPTCommands()
773 try:
774 getattr(commands, args.command.capitalize())(args)
775 except Exception as e:
776 if args.verbose or args.debug:
777 logging.exception('Failure in command [%s]', args.command)
778 exit('ERROR: %s' % e)
779
780
781if __name__ == '__main__':
782 main()