You-Cheng Syu | d569294 | 2018-01-04 14:40:59 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 2 | # 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 | |
| 8 | Chromium OS factory software usually needs to access partitions from disk |
| 9 | images. However, there is no good, lightweight, and portable GPT utility. |
| 10 | Most Chromium OS systems use `cgpt`, but that's not by default installed on |
| 11 | Ubuntu. Most systems have parted (GNU) or partx (util-linux-ng) but they have |
| 12 | their own problems. |
| 13 | |
| 14 | For example, when a disk image is resized (usually enlarged for putting more |
| 15 | resources 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 | |
| 22 | As a result, we need a dedicated tool to help processing GPT. |
| 23 | |
| 24 | This pygpt.py provides a simple and customized implementation for processing |
| 25 | GPT, as a replacement for `cgpt`. |
| 26 | """ |
| 27 | |
| 28 | |
| 29 | from __future__ import print_function |
| 30 | |
| 31 | import argparse |
| 32 | import binascii |
| 33 | import collections |
| 34 | import logging |
| 35 | import os |
| 36 | import struct |
| 37 | import 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 |
| 43 | HEADER_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 |
| 61 | PARTITION_FORMAT = """ |
| 62 | 16s TypeGUID |
| 63 | 16s UniqueGUID |
| 64 | Q FirstLBA |
| 65 | Q LastLBA |
| 66 | Q Attributes |
| 67 | 72s Names |
| 68 | """ |
| 69 | |
| 70 | |
| 71 | def 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 | |
| 87 | class 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 102 | DEFAULT_BLOCK_SIZE = 512 |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 103 | 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 122 | self.block_size = self.DEFAULT_BLOCK_SIZE |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 123 | |
| 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 Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 139 | 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 172 | # 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 Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 180 | raise ValueError('Invalid signature in GPT header.') |
Hung-Te Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 181 | |
| 182 | f.seek(gpt.block_size * header.PartitionEntriesStartingLBA) |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 183 | 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 214 | blocks = size / self.block_size |
| 215 | if size % self.block_size: |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 216 | 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 225 | old_size = self.block_size * (self.header.BackupLBA + 1) |
| 226 | if new_size % self.block_size: |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 227 | raise ValueError('New file size %d is not valid for image files.' % |
| 228 | new_size) |
Hung-Te Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 229 | new_blocks = new_size / self.block_size |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 230 | if old_size != new_size: |
| 231 | logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).', |
Hung-Te Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 232 | new_size, new_blocks, old_size, old_size / self.block_size) |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 233 | 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 268 | return self.block_size * (self.header.LastUsableLBA - max_lba) |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 269 | |
| 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 323 | name, lba, lba * self.block_size) |
| 324 | f.seek(lba * self.block_size) |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 325 | 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 | |
| 341 | class 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 Shih | c7156ca | 2018-02-26 14:46:24 +0800 | [diff] [blame] | 349 | ('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 Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 359 | |
| 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 Shih | c7156ca | 2018-02-26 14:46:24 +0800 | [diff] [blame] | 389 | 'use --expand to expand partitions.', |
Hung-Te Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 390 | free_space, free_space / gpt.block_size) |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 391 | |
| 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 Lin | f148d32 | 2018-04-13 10:24:42 +0800 | [diff] [blame^] | 517 | f.seek(gpt.header.BackupLBA * gpt.block_size) |
Hung-Te Lin | c772e1a | 2017-04-14 16:50:50 +0800 | [diff] [blame] | 518 | 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 | |
| 563 | def 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 | |
| 587 | if __name__ == '__main__': |
| 588 | main() |