blob: 59437a9e82d1a4c6573d5445d6a19d39d901793c [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
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800227 TYPE_NAME_CHROMEOS_KERNEL = 'ChromeOS kernel'
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800228 TYPE_GUID_MAP = {
229 '00000000-0000-0000-0000-000000000000': 'Unused',
230 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7': 'Linux data',
231 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309': 'ChromeOS kernel',
232 '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC': 'ChromeOS rootfs',
233 '2E0A753D-9E48-43B0-8337-B15192CB1B5E': 'ChromeOS reserved',
234 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3': 'ChromeOS firmware',
235 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': 'EFI System Partition',
236 }
237 TYPE_GUID_LIST_BOOTABLE = [
238 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309', # ChromeOS kernel
239 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B', # EFI System Partition
240 ]
241
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800242 @GPTBlob(HEADER_DESCRIPTION)
243 class Header(GPTObject):
244 """Wrapper to Header in GPT."""
245 SIGNATURES = ['EFI PART', 'CHROMEOS']
246 SIGNATURE_IGNORE = 'IGNOREME'
247 DEFAULT_REVISION = '\x00\x00\x01\x00'
248
249 DEFAULT_PARTITION_ENTRIES = 128
250 DEFAULT_PARTITIONS_LBA = 2 # LBA 0 = MBR, LBA 1 = GPT Header.
251
252 def Clone(self, **dargs):
253 """Creates a new instance with modifications.
254
255 GPT objects are usually named tuples that are immutable, so the only way
256 to make changes is to create a new instance with modifications.
257
258 CRC32 is always updated but PartitionArrayCRC32 must be updated explicitly
259 since we can't track changes in GPT.partitions automatically.
260
261 Note since GPTHeader.Clone will always update CRC, we can only check and
262 compute CRC by super(GPT.Header, header).Clone, or header._replace.
263 """
264 dargs['CRC32'] = 0
265 header = super(GPT.Header, self).Clone(**dargs)
266 return super(GPT.Header, header).Clone(CRC32=binascii.crc32(header.blob))
267
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800268 @classmethod
269 def Create(cls, size, block_size, pad_blocks=0,
270 part_entries=DEFAULT_PARTITION_ENTRIES):
271 """Creates a header with default values.
272
273 Args:
274 size: integer of expected image size.
275 block_size: integer for size of each block (sector).
276 pad_blocks: number of preserved sectors between header and partitions.
277 part_entries: number of partitions to include in header.
278 """
279 part_entry_size = struct.calcsize(GPT.Partition.FORMAT)
280 parts_lba = cls.DEFAULT_PARTITIONS_LBA + pad_blocks
281 parts_bytes = part_entries * part_entry_size
282 parts_blocks = parts_bytes / block_size
283 if parts_bytes % block_size:
284 parts_blocks += 1
285 # PartitionsCRC32 must be updated later explicitly.
286 return cls.ReadFrom(None).Clone(
287 Signature=cls.SIGNATURES[0],
288 Revision=cls.DEFAULT_REVISION,
289 HeaderSize=struct.calcsize(cls.FORMAT),
290 CurrentLBA=1,
291 BackupLBA=size / block_size - 1,
292 FirstUsableLBA=parts_lba + parts_blocks,
293 LastUsableLBA=size / block_size - parts_blocks - parts_lba,
294 DiskGUID=uuid.uuid4().get_bytes(),
295 PartitionEntriesStartingLBA=parts_lba,
296 PartitionEntriesNumber=part_entries,
297 PartitionEntrySize=part_entry_size,
298 )
299
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800300 class PartitionAttributes(object):
301 """Wrapper for Partition.Attributes.
302
303 This can be created using Partition.attrs, but the changed properties won't
304 apply to underlying Partition until an explicit call with
305 Partition.Clone(Attributes=new_attrs).
306 """
307
308 def __init__(self, attrs):
309 self._attrs = attrs
310
311 @property
312 def raw(self):
313 """Returns the raw integer type attributes."""
314 return self._Get()
315
316 def _Get(self):
317 return self._attrs
318
319 def _Set(self, value):
320 self._attrs = value
321
322 successful = BitProperty(_Get, _Set, 56, 1)
323 tries = BitProperty(_Get, _Set, 52, 0xf)
324 priority = BitProperty(_Get, _Set, 48, 0xf)
325 legacy_boot = BitProperty(_Get, _Set, 2, 1)
326 required = BitProperty(_Get, _Set, 0, 1)
327
328 @GPTBlob(PARTITION_DESCRIPTION)
329 class Partition(GPTObject):
330 """The partition entry in GPT.
331
332 Please include following properties when creating a Partition object:
333 - image: a string for path to the image file the partition maps to.
334 - number: the 1-based partition number.
335 - block_size: an integer for size of each block (LBA, or sector).
336 """
337 NAMES_ENCODING = 'utf-16-le'
338 NAMES_LENGTH = 72
339
340 CLONE_CONVERTERS = {
341 # TODO(hungte) check if encoded name is too long.
342 'label': lambda l: (None if l is None else
343 ('Names', l.encode(GPT.Partition.NAMES_ENCODING))),
344 'TypeGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
345 'UniqueGUID': lambda v: v.bytes_le if isinstance(v, uuid.UUID) else v,
346 'Attributes': (
347 lambda v: v.raw if isinstance(v, GPT.PartitionAttributes) else v),
348 }
349
350 def __str__(self):
351 return '%s#%s' % (self.image, self.number)
352
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800353 @classmethod
354 def Create(cls, block_size, image, number):
355 """Creates a new partition entry with given meta data."""
356 part = cls.ReadFrom(
357 None, image=image, number=number, block_size=block_size)
358 return part
359
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800360 def IsUnused(self):
361 """Returns if the partition is unused and can be allocated."""
362 return self.TypeGUID == GPT.TYPE_GUID_UNUSED
363
364 @property
365 def blocks(self):
366 """Return size of partition in blocks (see block_size)."""
367 return self.LastLBA - self.FirstLBA + 1
368
369 @property
370 def offset(self):
371 """Returns offset to partition in bytes."""
372 return self.FirstLBA * self.block_size
373
374 @property
375 def size(self):
376 """Returns size of partition in bytes."""
377 return self.blocks * self.block_size
378
379 @property
380 def type_guid(self):
381 return uuid.UUID(bytes_le=self.TypeGUID)
382
383 @property
384 def unique_guid(self):
385 return uuid.UUID(bytes_le=self.UniqueGUID)
386
387 @property
388 def label(self):
389 """Returns the Names in decoded string type."""
390 return self.Names.decode(self.NAMES_ENCODING).strip('\0')
391
392 @property
393 def attrs(self):
394 return GPT.PartitionAttributes(self.Attributes)
395
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800396 def __init__(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800397 """GPT constructor.
398
399 See LoadFromFile for how it's usually used.
400 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800401 self.header = None
402 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800403 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800404
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800405 @classmethod
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800406 def Create(cls, image_name, size, block_size, pad_blocks=0):
407 """Creates a new GPT instance from given size and block_size.
408
409 Args:
410 image_name: a string of underlying disk image file name.
411 size: expected size of disk image.
412 block_size: size of each block (sector) in bytes.
413 pad_blocks: number of blocks between header and partitions array.
414 """
415 gpt = cls()
416 gpt.block_size = block_size
417 gpt.header = cls.Header.Create(size, block_size, pad_blocks)
418 gpt.partitions = [
419 cls.Partition.Create(block_size, image_name, i + 1)
420 for i in xrange(gpt.header.PartitionEntriesNumber)]
421 return gpt
422
423 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800424 def LoadFromFile(cls, image):
425 """Loads a GPT table from give disk image file object.
426
427 Args:
428 image: a string as file path or a file-like object to read from.
429 """
430 if isinstance(image, basestring):
431 with open(image, 'rb') as f:
432 return cls.LoadFromFile(f)
433
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800434 gpt = cls()
Hung-Te Linf148d322018-04-13 10:24:42 +0800435 # Try DEFAULT_BLOCK_SIZE, then 4K.
436 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800437 image.seek(block_size * 1)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800438 header = gpt.Header.ReadFrom(image)
439 if header.Signature in cls.Header.SIGNATURES:
Hung-Te Linf148d322018-04-13 10:24:42 +0800440 gpt.block_size = block_size
441 break
442 else:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800443 raise GPTError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800444
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800445 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800446 def ReadPartition(image, i):
447 p = gpt.Partition.ReadFrom(
448 image, image=image.name, number=i + 1, block_size=gpt.block_size)
449 return p
450
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800451 gpt.header = header
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800452 gpt.partitions = [
453 ReadPartition(image, i) for i in range(header.PartitionEntriesNumber)]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800454 return gpt
455
456 def GetValidPartitions(self):
457 """Returns the list of partitions before entry with empty type GUID.
458
459 In partition table, the first entry with empty type GUID indicates end of
460 valid partitions. In most implementations all partitions after that should
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800461 be zeroed. However, few implementations for example cgpt, may create
462 partitions in arbitrary order so use this carefully.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800463 """
464 for i, p in enumerate(self.partitions):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800465 if p.IsUnused():
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800466 return self.partitions[:i]
467 return self.partitions
468
469 def GetMaxUsedLBA(self):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800470 """Returns the max LastLBA from all used partitions."""
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800471 parts = [p for p in self.partitions if not p.IsUnused()]
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800472 return (max(p.LastLBA for p in parts)
473 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800474
475 def GetPartitionTableBlocks(self, header=None):
476 """Returns the blocks (or LBA) of partition table from given header."""
477 if header is None:
478 header = self.header
479 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800480 blocks = size / self.block_size
481 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800482 blocks += 1
483 return blocks
484
485 def Resize(self, new_size):
486 """Adjust GPT for a disk image in given size.
487
488 Args:
489 new_size: Integer for new size of disk image file.
490 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800491 old_size = self.block_size * (self.header.BackupLBA + 1)
492 if new_size % self.block_size:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800493 raise GPTError(
494 'New file size %d is not valid for image files.' % new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800495 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800496 if old_size != new_size:
497 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800498 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800499 else:
500 logging.info('Image size (%d, LBA=%d) not changed.',
501 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800502 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800503
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800504 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800505 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800506 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800507
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800508 if last_usable_lba < self.header.LastUsableLBA:
509 max_used_lba = self.GetMaxUsedLBA()
510 if last_usable_lba < max_used_lba:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800511 raise GPTError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800512
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800513 self.header = self.header.Clone(
514 BackupLBA=backup_lba, LastUsableLBA=last_usable_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800515
516 def GetFreeSpace(self):
517 """Returns the free (available) space left according to LastUsableLBA."""
518 max_lba = self.GetMaxUsedLBA()
519 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800520 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800521
522 def ExpandPartition(self, i):
523 """Expands a given partition to last usable LBA.
524
525 Args:
526 i: Index (0-based) of target partition.
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800527
528 Returns:
529 (old_blocks, new_blocks) for size in blocks.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800530 """
531 # Assume no partitions overlap, we need to make sure partition[i] has
532 # largest LBA.
533 if i < 0 or i >= len(self.GetValidPartitions()):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800534 raise GPTError('Partition number %d is invalid.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800535 p = self.partitions[i]
536 max_used_lba = self.GetMaxUsedLBA()
537 if max_used_lba > p.LastLBA:
Hung-Te Lin4dfd3302018-04-17 14:47:52 +0800538 raise GPTError(
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800539 'Cannot expand partition %d because it is not the last allocated '
540 'partition.' % (i + 1))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800541
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800542 old_blocks = p.blocks
543 p = p.Clone(LastLBA=self.header.LastUsableLBA)
544 new_blocks = p.blocks
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800545 self.partitions[i] = p
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800546 logging.warn(
547 '%s expanded, size in LBA: %d -> %d.', p, old_blocks, new_blocks)
548 return (old_blocks, new_blocks)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800549
550 def UpdateChecksum(self):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800551 """Updates all checksum fields in GPT objects.
552
553 The Header.CRC32 is automatically updated in Header.Clone().
554 """
555 parts = ''.join(p.blob for p in self.partitions)
556 self.header = self.header.Clone(
557 PartitionArrayCRC32=binascii.crc32(parts))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800558
559 def GetBackupHeader(self):
560 """Returns the backup header according to current header."""
561 partitions_starting_lba = (
562 self.header.BackupLBA - self.GetPartitionTableBlocks())
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800563 return self.header.Clone(
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800564 BackupLBA=self.header.CurrentLBA,
565 CurrentLBA=self.header.BackupLBA,
566 PartitionEntriesStartingLBA=partitions_starting_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800567
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800568 def WriteToFile(self, image):
569 """Updates partition table in a disk image file.
570
571 Args:
572 image: a string as file path or a file-like object to write into.
573 """
574 if isinstance(image, basestring):
575 with open(image, 'rb+') as f:
576 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800577
578 def WriteData(name, blob, lba):
579 """Writes a blob into given location."""
580 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800581 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800582 image.seek(lba * self.block_size)
583 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800584
585 self.UpdateChecksum()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800586 parts_blob = ''.join(p.blob for p in self.partitions)
587 WriteData('GPT Header', self.header.blob, self.header.CurrentLBA)
588 WriteData(
589 'GPT Partitions', parts_blob, self.header.PartitionEntriesStartingLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800590 logging.info('Usable LBA: First=%d, Last=%d',
591 self.header.FirstUsableLBA, self.header.LastUsableLBA)
592 backup_header = self.GetBackupHeader()
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800593 WriteData(
594 'Backup Partitions', parts_blob,
595 backup_header.PartitionEntriesStartingLBA)
596 WriteData('Backup Header', backup_header.blob, backup_header.CurrentLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800597
598
599class GPTCommands(object):
600 """Collection of GPT sub commands for command line to use.
601
602 The commands are derived from `cgpt`, but not necessary to be 100% compatible
603 with cgpt.
604 """
605
606 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800607 ('begin', 'beginning sector'),
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800608 ('size', 'partition size (in sectors)'),
Peter Shihc7156ca2018-02-26 14:46:24 +0800609 ('type', 'type guid'),
610 ('unique', 'unique guid'),
611 ('label', 'label'),
612 ('Successful', 'Successful flag'),
613 ('Tries', 'Tries flag'),
614 ('Priority', 'Priority flag'),
615 ('Legacy', 'Legacy Boot flag'),
616 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800617
618 def __init__(self):
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800619 commands = dict(
620 (command.lower(), getattr(self, command)())
621 for command in dir(self)
622 if (isinstance(getattr(self, command), type) and
623 issubclass(getattr(self, command), self.SubCommand) and
624 getattr(self, command) is not self.SubCommand)
625 )
626 self.commands = commands
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800627
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800628 def DefineArgs(self, parser):
629 """Defines all available commands to an argparser subparsers instance."""
630 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
631 for name, instance in sorted(self.commands.iteritems()):
632 parser = subparsers.add_parser(
633 name, description=instance.__doc__,
634 formatter_class=argparse.RawDescriptionHelpFormatter,
635 help=instance.__doc__.splitlines()[0])
636 instance.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800637
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800638 def Execute(self, args):
639 """Execute the sub commands by given parsed arguments."""
640 self.commands[args.command].Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800641
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800642 class SubCommand(object):
643 """A base class for sub commands to derive from."""
644
645 def DefineArgs(self, parser):
646 """Defines command line arguments to argparse parser.
647
648 Args:
649 parser: An argparse parser instance.
650 """
651 del parser # Unused.
652 raise NotImplementedError
653
654 def Execute(self, args):
655 """Execute the command.
656
657 Args:
658 args: An argparse parsed namespace.
659 """
660 del args # Unused.
661 raise NotImplementedError
662
Hung-Te Lin6c3575a2018-04-17 15:00:49 +0800663 class Create(SubCommand):
664 """Create or reset GPT headers and tables.
665
666 Create or reset an empty GPT.
667 """
668
669 def DefineArgs(self, parser):
670 parser.add_argument(
671 '-z', '--zero', action='store_true',
672 help='Zero the sectors of the GPT table and entries')
673 parser.add_argument(
674 '-p', '--pad_blocks', type=int, default=0,
675 help=('Size (in blocks) of the disk to pad between the '
676 'primary GPT header and its entries, default %(default)s'))
677 parser.add_argument(
678 '--block_size', type=int, default=GPT.DEFAULT_BLOCK_SIZE,
679 help='Size of each block (sector) in bytes.')
680 parser.add_argument(
681 'image_file', type=argparse.FileType('rb+'),
682 help='Disk image file to create.')
683
684 def Execute(self, args):
685 block_size = args.block_size
686 gpt = GPT.Create(
687 args.image_file.name, os.path.getsize(args.image_file.name),
688 block_size, args.pad_blocks)
689 if args.zero:
690 # In theory we only need to clear LBA 1, but to make sure images already
691 # initialized with different block size won't have GPT signature in
692 # different locations, we should zero until first usable LBA.
693 args.image_file.seek(0)
694 args.image_file.write('\0' * block_size * gpt.header.FirstUsableLBA)
695 gpt.WriteToFile(args.image_file)
696 print('OK: Created GPT for %s' % args.image_file.name)
697
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800698 class Repair(SubCommand):
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800699 """Repair damaged GPT headers and tables."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800700
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800701 def DefineArgs(self, parser):
702 parser.add_argument(
703 'image_file', type=argparse.FileType('rb+'),
704 help='Disk image file to repair.')
705
706 def Execute(self, args):
707 gpt = GPT.LoadFromFile(args.image_file)
708 gpt.Resize(os.path.getsize(args.image_file.name))
709 gpt.WriteToFile(args.image_file)
710 print('Disk image file %s repaired.' % args.image_file.name)
711
712 class Expand(SubCommand):
713 """Expands a GPT partition to all available free space."""
714
715 def DefineArgs(self, parser):
716 parser.add_argument(
717 '-i', '--number', type=int, required=True,
718 help='The partition to expand.')
719 parser.add_argument(
720 'image_file', type=argparse.FileType('rb+'),
721 help='Disk image file to modify.')
722
723 def Execute(self, args):
724 gpt = GPT.LoadFromFile(args.image_file)
725 old_blocks, new_blocks = gpt.ExpandPartition(args.number - 1)
726 gpt.WriteToFile(args.image_file)
727 if old_blocks < new_blocks:
728 print(
729 'Partition %s on disk image file %s has been extended '
730 'from %s to %s .' %
731 (args.number, args.image_file.name, old_blocks * gpt.block_size,
732 new_blocks * gpt.block_size))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800733 else:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800734 print('Nothing to expand for disk image %s partition %s.' %
735 (args.image_file.name, args.number))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800736
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800737 class Show(SubCommand):
738 """Show partition table and entries.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800739
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800740 Display the GPT table.
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800741 """
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800742
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800743 def DefineArgs(self, parser):
744 parser.add_argument(
745 '--numeric', '-n', action='store_true',
746 help='Numeric output only.')
747 parser.add_argument(
748 '--quick', '-q', action='store_true',
749 help='Quick output.')
750 parser.add_argument(
751 '-i', '--number', type=int,
752 help='Show specified partition only, with format args.')
753 for name, help_str in GPTCommands.FORMAT_ARGS:
754 # TODO(hungte) Alert if multiple args were specified.
755 parser.add_argument(
756 '--%s' % name, '-%c' % name[0], action='store_true',
757 help='[format] %s.' % help_str)
758 parser.add_argument(
759 'image_file', type=argparse.FileType('rb'),
760 help='Disk image file to show.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800761
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800762 def Execute(self, args):
763 """Show partition table and entries."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800764
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800765 def FormatGUID(bytes_le):
766 return str(uuid.UUID(bytes_le=bytes_le)).upper()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800767
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800768 def FormatTypeGUID(p):
769 guid_str = FormatGUID(p.TypeGUID)
770 if not args.numeric:
771 names = gpt.TYPE_GUID_MAP.get(guid_str)
772 if names:
773 return names
774 return guid_str
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800775
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800776 def FormatNames(p):
Hung-Te Lin49ac3c22018-04-17 14:37:54 +0800777 return p.label
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800778
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800779 def IsBootableType(type_guid):
780 return type_guid in gpt.TYPE_GUID_LIST_BOOTABLE
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800781
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800782 def FormatAttribute(attrs, chromeos_kernel=False):
783 if args.numeric:
784 return '[%x]' % (attrs.raw >> 48)
785 results = []
786 if chromeos_kernel:
787 results += [
788 'priority=%d' % attrs.priority,
789 'tries=%d' % attrs.tries,
790 'successful=%d' % attrs.successful]
791 if attrs.required:
792 results += ['required=1']
793 if attrs.legacy_boot:
794 results += ['legacy_boot=1']
795 return ' '.join(results)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800796
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800797 def ApplyFormatArgs(p):
798 if args.begin:
799 return p.FirstLBA
800 elif args.size:
801 return p.blocks
802 elif args.type:
803 return FormatTypeGUID(p)
804 elif args.unique:
805 return FormatGUID(p.UniqueGUID)
806 elif args.label:
807 return FormatNames(p)
808 elif args.Successful:
809 return p.attrs.successful
810 elif args.Priority:
811 return p.attrs.priority
812 elif args.Tries:
813 return p.attrs.tries
814 elif args.Legacy:
815 return p.attrs.legacy_boot
816 elif args.Attribute:
817 return '[%x]' % (p.Attributes >> 48)
818 else:
819 return None
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800820
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800821 def IsFormatArgsSpecified():
822 return any(getattr(args, arg[0]) for arg in GPTCommands.FORMAT_ARGS)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800823
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800824 gpt = GPT.LoadFromFile(args.image_file)
825 logging.debug('%r', gpt.header)
826 fmt = '%12s %11s %7s %s'
827 fmt2 = '%32s %s: %s'
828 header = ('start', 'size', 'part', 'contents')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800829
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800830 if IsFormatArgsSpecified() and args.number is None:
831 raise GPTError('Format arguments must be used with -i.')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800832
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800833 if not (args.number is None or
834 0 < args.number <= gpt.header.PartitionEntriesNumber):
835 raise GPTError('Invalid partition number: %d' % args.number)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800836
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800837 partitions = gpt.partitions
838 do_print_gpt_blocks = False
839 if not (args.quick or IsFormatArgsSpecified()):
840 print(fmt % header)
841 if args.number is None:
842 do_print_gpt_blocks = True
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800843
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800844 if do_print_gpt_blocks:
845 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
846 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
847 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800848
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800849 for p in partitions:
850 if args.number is None:
851 # Skip unused partitions.
852 if p.IsUnused():
853 continue
854 elif p.number != args.number:
855 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800856
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800857 if IsFormatArgsSpecified():
858 print(ApplyFormatArgs(p))
859 continue
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800860
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800861 type_guid = FormatGUID(p.TypeGUID)
862 print(fmt % (p.FirstLBA, p.blocks, p.number,
863 FormatTypeGUID(p) if args.quick else
864 'Label: "%s"' % FormatNames(p)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800865
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800866 if not args.quick:
867 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
868 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
869 if args.numeric or IsBootableType(type_guid):
870 name = GPT.TYPE_GUID_MAP[type_guid]
871 print(fmt2 % ('', 'Attr', FormatAttribute(
872 p.attrs, name == GPT.TYPE_NAME_CHROMEOS_KERNEL)))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800873
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800874 if do_print_gpt_blocks:
875 f = args.image_file
876 f.seek(gpt.header.BackupLBA * gpt.block_size)
877 header = gpt.Header.ReadFrom(f)
878 print(fmt % (header.PartitionEntriesStartingLBA,
879 gpt.GetPartitionTableBlocks(header), '',
880 'Sec GPT table'))
881 print(fmt % (header.CurrentLBA, 1, '', 'Sec GPT header'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800882
883
884def main():
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800885 commands = GPTCommands()
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800886 parser = argparse.ArgumentParser(description='GPT Utility.')
887 parser.add_argument('--verbose', '-v', action='count', default=0,
888 help='increase verbosity.')
889 parser.add_argument('--debug', '-d', action='store_true',
890 help='enable debug output.')
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800891 commands.DefineArgs(parser)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800892
893 args = parser.parse_args()
894 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
895 if args.debug:
896 log_level = logging.DEBUG
897 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
898 level=log_level)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800899 try:
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800900 commands.Execute(args)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800901 except Exception as e:
902 if args.verbose or args.debug:
903 logging.exception('Failure in command [%s]', args.command)
Hung-Te Lin5cb0c312018-04-17 14:56:43 +0800904 exit('ERROR: %s: %s' % (args.command, str(e) or 'Unknown error.'))
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800905
906
907if __name__ == '__main__':
908 main()