blob: 608c0e841874d17724078370826438640643eb1d [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
43HEADER_FORMAT = """
44 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
61PARTITION_FORMAT = """
62 16s TypeGUID
63 16s UniqueGUID
64 Q FirstLBA
65 Q LastLBA
66 Q Attributes
67 72s Names
68"""
69
70
71def BuildStructFormatAndNamedTuple(name, description):
72 """Builds the format string for struct and create corresponding namedtuple.
73
74 Args:
75 name: A string for name of the named tuple.
76 description: A string with struct descriptor and attribute name.
77
78 Returns:
79 A pair of (struct_format, namedtuple_class).
80 """
81 elements = description.split()
82 struct_format = '<' + ''.join(elements[::2])
83 tuple_class = collections.namedtuple(name, elements[1::2])
84 return (struct_format, tuple_class)
85
86
87class GPT(object):
88 """A GPT helper class.
89
90 To load GPT from an existing disk image file, use `LoadFromFile`.
91 After modifications were made, use `WriteToFile` to commit changes.
92
93 Attributes:
94 header: A namedtuple of GPT header.
95 partitions: A list of GPT partition entry nametuple.
96 """
97
98 HEADER_FORMAT, HEADER_CLASS = BuildStructFormatAndNamedTuple(
99 'Header', HEADER_FORMAT)
100 PARTITION_FORMAT, PARTITION_CLASS = BuildStructFormatAndNamedTuple(
101 'Partition', PARTITION_FORMAT)
Hung-Te Linf148d322018-04-13 10:24:42 +0800102 DEFAULT_BLOCK_SIZE = 512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800103 HEADER_SIGNATURE = 'EFI PART'
104 TYPE_GUID_UNUSED = '\x00' * 16
105 TYPE_GUID_MAP = {
106 '00000000-0000-0000-0000-000000000000': 'Unused',
107 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7': 'Linux data',
108 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309': 'ChromeOS kernel',
109 '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC': 'ChromeOS rootfs',
110 '2E0A753D-9E48-43B0-8337-B15192CB1B5E': 'ChromeOS reserved',
111 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3': 'ChromeOS firmware',
112 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': 'EFI System Partition',
113 }
114 TYPE_GUID_LIST_BOOTABLE = [
115 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309', # ChromeOS kernel
116 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B', # EFI System Partition
117 ]
118
119 def __init__(self):
120 self.header = None
121 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800122 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800123
124 @staticmethod
125 def GetAttributeSuccess(attrs):
126 return (attrs >> 56) & 1
127
128 @staticmethod
129 def GetAttributeTries(attrs):
130 return (attrs >> 52) & 0xf
131
132 @staticmethod
133 def GetAttributePriority(attrs):
134 return (attrs >> 48) & 0xf
135
136 @staticmethod
137 def NewNamedTuple(base, **dargs):
138 """Builds a new named tuple based on dargs."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800139 return base._replace(**dargs)
140
141 @classmethod
142 def ReadHeader(cls, f):
143 return cls.HEADER_CLASS(*struct.unpack(
144 cls.HEADER_FORMAT, f.read(struct.calcsize(cls.HEADER_FORMAT))))
145
146 @classmethod
147 def ReadPartitionEntry(cls, f):
148 return cls.PARTITION_CLASS(*struct.unpack(
149 cls.PARTITION_FORMAT, f.read(struct.calcsize(cls.PARTITION_FORMAT))))
150
151 @classmethod
152 def GetHeaderBlob(cls, header):
153 return struct.pack(cls.HEADER_FORMAT, *header)
154
155 @classmethod
156 def GetHeaderCRC32(cls, header):
157 return binascii.crc32(cls.GetHeaderBlob(cls.NewNamedTuple(header, CRC32=0)))
158
159 @classmethod
160 def GetPartitionsBlob(cls, partitions):
161 return ''.join(struct.pack(cls.PARTITION_FORMAT, *partition)
162 for partition in partitions)
163
164 @classmethod
165 def GetPartitionsCRC32(cls, partitions):
166 return binascii.crc32(cls.GetPartitionsBlob(partitions))
167
168 @classmethod
169 def LoadFromFile(cls, f):
170 """Loads a GPT table from give disk image file object."""
171 gpt = GPT()
Hung-Te Linf148d322018-04-13 10:24:42 +0800172 # Try DEFAULT_BLOCK_SIZE, then 4K.
173 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
174 f.seek(block_size * 1)
175 header = gpt.ReadHeader(f)
176 if header.Signature == cls.HEADER_SIGNATURE:
177 gpt.block_size = block_size
178 break
179 else:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800180 raise ValueError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800181
182 f.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800183 partitions = [gpt.ReadPartitionEntry(f)
184 for unused_i in range(header.PartitionEntriesNumber)]
185 gpt.header = header
186 gpt.partitions = partitions
187 return gpt
188
189 def GetValidPartitions(self):
190 """Returns the list of partitions before entry with empty type GUID.
191
192 In partition table, the first entry with empty type GUID indicates end of
193 valid partitions. In most implementations all partitions after that should
194 be zeroed.
195 """
196 for i, p in enumerate(self.partitions):
197 if p.TypeGUID == self.TYPE_GUID_UNUSED:
198 return self.partitions[:i]
199 return self.partitions
200
201 def GetMaxUsedLBA(self):
202 """Returns the max LastLBA from all valid partitions."""
203 return max(p.LastLBA for p in self.GetValidPartitions())
204
205 def GetMinUsedLBA(self):
206 """Returns the min FirstLBA from all valid partitions."""
207 return min(p.FirstLBA for p in self.GetValidPartitions())
208
209 def GetPartitionTableBlocks(self, header=None):
210 """Returns the blocks (or LBA) of partition table from given header."""
211 if header is None:
212 header = self.header
213 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800214 blocks = size / self.block_size
215 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800216 blocks += 1
217 return blocks
218
219 def Resize(self, new_size):
220 """Adjust GPT for a disk image in given size.
221
222 Args:
223 new_size: Integer for new size of disk image file.
224 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800225 old_size = self.block_size * (self.header.BackupLBA + 1)
226 if new_size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800227 raise ValueError('New file size %d is not valid for image files.' %
228 new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800229 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800230 if old_size != new_size:
231 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800232 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800233 else:
234 logging.info('Image size (%d, LBA=%d) not changed.',
235 new_size, new_blocks)
236
237 # Re-calculate all related fields.
238 backup_lba = new_blocks - 1
239 partitions_blocks = self.GetPartitionTableBlocks()
240
241 # To add allow adding more blocks for partition table, we should reserve
242 # same space between primary and backup partition tables and real
243 # partitions.
244 min_used_lba = self.GetMinUsedLBA()
245 max_used_lba = self.GetMaxUsedLBA()
246 primary_reserved = min_used_lba - self.header.PartitionEntriesStartingLBA
247 primary_last_lba = (self.header.PartitionEntriesStartingLBA +
248 partitions_blocks - 1)
249
250 if primary_last_lba >= min_used_lba:
251 raise ValueError('Partition table overlaps partitions.')
252 if max_used_lba + partitions_blocks >= backup_lba:
253 raise ValueError('Partitions overlaps backup partition table.')
254
255 last_usable_lba = backup_lba - primary_reserved - 1
256 if last_usable_lba < max_used_lba:
257 last_usable_lba = max_used_lba
258
259 self.header = self.NewNamedTuple(
260 self.header,
261 BackupLBA=backup_lba,
262 LastUsableLBA=last_usable_lba)
263
264 def GetFreeSpace(self):
265 """Returns the free (available) space left according to LastUsableLBA."""
266 max_lba = self.GetMaxUsedLBA()
267 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800268 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800269
270 def ExpandPartition(self, i):
271 """Expands a given partition to last usable LBA.
272
273 Args:
274 i: Index (0-based) of target partition.
275 """
276 # Assume no partitions overlap, we need to make sure partition[i] has
277 # largest LBA.
278 if i < 0 or i >= len(self.GetValidPartitions()):
279 raise ValueError('Partition index %d is invalid.' % (i + 1))
280 p = self.partitions[i]
281 max_used_lba = self.GetMaxUsedLBA()
282 if max_used_lba > p.LastLBA:
283 raise ValueError('Cannot expand partition %d because it is not the last '
284 'allocated partition.' % (i + 1))
285
286 old_blocks = p.LastLBA - p.FirstLBA + 1
287 p = self.NewNamedTuple(p, LastLBA=self.header.LastUsableLBA)
288 new_blocks = p.LastLBA - p.FirstLBA + 1
289 self.partitions[i] = p
290 logging.warn('Partition NR=%d expanded, size in LBA: %d -> %d.',
291 i + 1, old_blocks, new_blocks)
292
293 def UpdateChecksum(self):
294 """Updates all checksum values in GPT header."""
295 header = self.NewNamedTuple(
296 self.header,
297 CRC32=0,
298 PartitionArrayCRC32=self.GetPartitionsCRC32(self.partitions))
299 self.header = self.NewNamedTuple(
300 header,
301 CRC32=self.GetHeaderCRC32(header))
302
303 def GetBackupHeader(self):
304 """Returns the backup header according to current header."""
305 partitions_starting_lba = (
306 self.header.BackupLBA - self.GetPartitionTableBlocks())
307 header = self.NewNamedTuple(
308 self.header,
309 CRC32=0,
310 BackupLBA=self.header.CurrentLBA,
311 CurrentLBA=self.header.BackupLBA,
312 PartitionEntriesStartingLBA=partitions_starting_lba)
313 return self.NewNamedTuple(
314 header,
315 CRC32=self.GetHeaderCRC32(header))
316
317 def WriteToFile(self, f):
318 """Updates partition table in a disk image file."""
319
320 def WriteData(name, blob, lba):
321 """Writes a blob into given location."""
322 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800323 name, lba, lba * self.block_size)
324 f.seek(lba * self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800325 f.write(blob)
326
327 self.UpdateChecksum()
328 WriteData('GPT Header', self.GetHeaderBlob(self.header),
329 self.header.CurrentLBA)
330 WriteData('GPT Partitions', self.GetPartitionsBlob(self.partitions),
331 self.header.PartitionEntriesStartingLBA)
332 logging.info('Usable LBA: First=%d, Last=%d',
333 self.header.FirstUsableLBA, self.header.LastUsableLBA)
334 backup_header = self.GetBackupHeader()
335 WriteData('Backup Partitions', self.GetPartitionsBlob(self.partitions),
336 backup_header.PartitionEntriesStartingLBA)
337 WriteData('Backup Header', self.GetHeaderBlob(backup_header),
338 backup_header.CurrentLBA)
339
340
341class GPTCommands(object):
342 """Collection of GPT sub commands for command line to use.
343
344 The commands are derived from `cgpt`, but not necessary to be 100% compatible
345 with cgpt.
346 """
347
348 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800349 ('begin', 'beginning sector'),
350 ('size', 'partition size'),
351 ('type', 'type guid'),
352 ('unique', 'unique guid'),
353 ('label', 'label'),
354 ('Successful', 'Successful flag'),
355 ('Tries', 'Tries flag'),
356 ('Priority', 'Priority flag'),
357 ('Legacy', 'Legacy Boot flag'),
358 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800359
360 def __init__(self):
361 pass
362
363 @classmethod
364 def RegisterRepair(cls, p):
365 """Registers the repair command to argparser.
366
367 Args:
368 p: An argparse parser instance.
369 """
370 p.add_argument(
371 '--expand', action='store_true', default=False,
372 help='Expands stateful partition to full disk.')
373 p.add_argument('image_file', type=argparse.FileType('rb+'),
374 help='Disk image file to repair.')
375
376 def Repair(self, args):
377 """Repair damaged GPT headers and tables."""
378 gpt = GPT.LoadFromFile(args.image_file)
379 gpt.Resize(os.path.getsize(args.image_file.name))
380
381 free_space = gpt.GetFreeSpace()
382 if args.expand:
383 if free_space:
384 gpt.ExpandPartition(0)
385 else:
386 logging.warn('- No extra space to expand.')
387 elif free_space:
388 logging.warn('Extra space found (%d, LBA=%d), '
Peter Shihc7156ca2018-02-26 14:46:24 +0800389 'use --expand to expand partitions.',
Hung-Te Linf148d322018-04-13 10:24:42 +0800390 free_space, free_space / gpt.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800391
392 gpt.WriteToFile(args.image_file)
393 print('Disk image file %s repaired.' % args.image_file.name)
394
395 @classmethod
396 def RegisterShow(cls, p):
397 """Registers the repair command to argparser.
398
399 Args:
400 p: An argparse parser instance.
401 """
402 p.add_argument('--numeric', '-n', action='store_true',
403 help='Numeric output only.')
404 p.add_argument('--quick', '-q', action='store_true',
405 help='Quick output.')
406 p.add_argument('--index', '-i', type=int, default=None,
407 help='Show specified partition only, with format args.')
408 for name, help_str in cls.FORMAT_ARGS:
409 # TODO(hungte) Alert if multiple args were specified.
410 p.add_argument('--%s' % name, '-%c' % name[0], action='store_true',
411 help='[format] %s.' % help_str)
412 p.add_argument('image_file', type=argparse.FileType('rb'),
413 help='Disk image file to show.')
414
415
416 def Show(self, args):
417 """Show partition table and entries."""
418
419 def FormatGUID(bytes_le):
420 return str(uuid.UUID(bytes_le=bytes_le)).upper()
421
422 def FormatTypeGUID(p):
423 guid_str = FormatGUID(p.TypeGUID)
424 if not args.numeric:
425 names = gpt.TYPE_GUID_MAP.get(guid_str)
426 if names:
427 return names
428 return guid_str
429
430 def FormatNames(p):
431 return p.Names.decode('utf-16-le').strip('\0')
432
433 def IsBootableType(type_guid):
434 return type_guid in gpt.TYPE_GUID_LIST_BOOTABLE
435
436 def FormatAttribute(attr):
437 if args.numeric:
438 return '[%x]' % (attr >> 48)
439 if attr & 4:
440 return 'legacy_boot=1'
441 return 'priority=%d tries=%d successful=%d' % (
442 gpt.GetAttributePriority(attr),
443 gpt.GetAttributeTries(attr),
444 gpt.GetAttributeSuccess(attr))
445
446 def ApplyFormatArgs(p):
447 if args.begin:
448 return p.FirstLBA
449 elif args.size:
450 return p.LastLBA - p.FirstLBA + 1
451 elif args.type:
452 return FormatTypeGUID(p)
453 elif args.unique:
454 return FormatGUID(p.UniqueGUID)
455 elif args.label:
456 return FormatNames(p)
457 elif args.Successful:
458 return gpt.GetAttributeSuccess(p.Attributes)
459 elif args.Priority:
460 return gpt.GetAttributePriority(p.Attributes)
461 elif args.Tries:
462 return gpt.GetAttributeTries(p.Attributes)
463 elif args.Legacy:
464 raise NotImplementedError
465 elif args.Attribute:
466 return '[%x]' % (p.Attributes >> 48)
467 else:
468 return None
469
470 def IsFormatArgsSpecified():
471 return any(getattr(args, arg[0]) for arg in self.FORMAT_ARGS)
472
473 gpt = GPT.LoadFromFile(args.image_file)
474 fmt = '%12s %11s %7s %s'
475 fmt2 = '%32s %s: %s'
476 header = ('start', 'size', 'part', 'contents')
477
478 if IsFormatArgsSpecified() and args.index is None:
479 raise ValueError('Format arguments must be used with -i.')
480
481 partitions = gpt.GetValidPartitions()
482 if not (args.index is None or 0 < args.index <= len(partitions)):
483 raise ValueError('Invalid partition index: %d' % args.index)
484
485 do_print_gpt_blocks = False
486 if not (args.quick or IsFormatArgsSpecified()):
487 print(fmt % header)
488 if args.index is None:
489 do_print_gpt_blocks = True
490
491 if do_print_gpt_blocks:
492 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
493 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
494 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
495
496 for i, p in enumerate(partitions):
497 if args.index is not None and i != args.index - 1:
498 continue
499
500 if IsFormatArgsSpecified():
501 print(ApplyFormatArgs(p))
502 continue
503
504 type_guid = FormatGUID(p.TypeGUID)
505 print(fmt % (p.FirstLBA, p.LastLBA - p.FirstLBA + 1, i + 1,
506 FormatTypeGUID(p) if args.quick else
507 'Label: "%s"' % FormatNames(p)))
508
509 if not args.quick:
510 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
511 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
512 if args.numeric or IsBootableType(type_guid):
513 print(fmt2 % ('', 'Attr', FormatAttribute(p.Attributes)))
514
515 if do_print_gpt_blocks:
516 f = args.image_file
Hung-Te Linf148d322018-04-13 10:24:42 +0800517 f.seek(gpt.header.BackupLBA * gpt.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800518 backup_header = gpt.ReadHeader(f)
519 print(fmt % (backup_header.PartitionEntriesStartingLBA,
520 gpt.GetPartitionTableBlocks(backup_header), '',
521 'Sec GPT table'))
522 print(fmt % (gpt.header.BackupLBA, 1, '', 'Sec GPT header'))
523
524 def Create(self, args):
525 """Create or reset GPT headers and tables."""
526 del args # Not used yet.
527 raise NotImplementedError
528
529 def Add(self, args):
530 """Add, edit or remove a partition entry."""
531 del args # Not used yet.
532 raise NotImplementedError
533
534 def Boot(self, args):
535 """Edit the PMBR sector for legacy BIOSes."""
536 del args # Not used yet.
537 raise NotImplementedError
538
539 def Find(self, args):
540 """Locate a partition by its GUID."""
541 del args # Not used yet.
542 raise NotImplementedError
543
544 def Prioritize(self, args):
545 """Reorder the priority of all kernel partitions."""
546 del args # Not used yet.
547 raise NotImplementedError
548
549 def Legacy(self, args):
550 """Switch between GPT and Legacy GPT."""
551 del args # Not used yet.
552 raise NotImplementedError
553
554 @classmethod
555 def RegisterAllCommands(cls, subparsers):
556 """Registers all available commands to an argparser subparsers instance."""
557 subcommands = [('show', cls.Show, cls.RegisterShow),
558 ('repair', cls.Repair, cls.RegisterRepair)]
559 for name, invocation, register_command in subcommands:
560 register_command(subparsers.add_parser(name, help=invocation.__doc__))
561
562
563def main():
564 parser = argparse.ArgumentParser(description='GPT Utility.')
565 parser.add_argument('--verbose', '-v', action='count', default=0,
566 help='increase verbosity.')
567 parser.add_argument('--debug', '-d', action='store_true',
568 help='enable debug output.')
569 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
570 GPTCommands.RegisterAllCommands(subparsers)
571
572 args = parser.parse_args()
573 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
574 if args.debug:
575 log_level = logging.DEBUG
576 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
577 level=log_level)
578 commands = GPTCommands()
579 try:
580 getattr(commands, args.command.capitalize())(args)
581 except Exception as e:
582 if args.verbose or args.debug:
583 logging.exception('Failure in command [%s]', args.command)
584 exit('ERROR: %s' % e)
585
586
587if __name__ == '__main__':
588 main()