#!/usr/bin/perl -w # Change the size of a GPT image's partition array. # The vast majority of GPT partition tables have an 128-entry partition array. # However, we get reports of ZFS-related arrays with a mere 9 entries, and # some others with 140, and parted could not handle either of those because # it was effectively hard-coding the 128. # This script takes as input a GPT image that might be created by # parted itself, and transforms it into one with a different number, N, # of partition array entries. That involves the following steps: # - poke that value of N into the 4-byte "Number of partition entries" slot, # - compute the crc32 of the N partition table entries, # - poke the resulting value into its slot, # - recompute the GPT header's CRC32 checksum and poke that into its slot. # Do the above for both the primary and the backup GPT headers. use strict; use warnings; use Digest::CRC qw(crc32); use List::Util qw(max); (my $ME = $0) =~ s|.*/||; my $VERSION = '1.0'; # Technically we shouldn't hard-code this, since it's specified # as the little-endian number in bytes 12..15 of the GPT header. my $gpt_header_len = 92; # Size of a partition array entry, in bytes. # This too is specified in the GPT header, but AFAIK, no one changes it. my $pe_size = 128; # Sector size. my $ss; # Sector number of the backup GPT header, to be read from the primary header. my $backup_LBA; # Given a GPT header $B, extract the my_LBA/backup_LBA sector number. sub curr_LBA($) { my ($b) = @_; unpack ('Q<', substr ($b, 24, 8)) } sub backup_LBA($) { my ($b) = @_; unpack ('Q<', substr ($b, 32, 8)) } # Given a GPT header $B, return its "partition entries starting LBA". sub pe_start_LBA($) { my ($b) = @_; unpack ('Q<', substr ($b, 72, 8)) } sub round_up_to_ss ($) { my ($n) = @_; return $n + $ss - $n % $ss; } # Return the byte offset of the start of the specified partition array. sub partition_array_start_offset ($$) { my ($pri_or_backup, $n_pe) = @_; $pri_or_backup eq 'primary' and return 2 * $ss; # Backup return $backup_LBA * $ss - round_up_to_ss ($n_pe * $pe_size); } # Calculate and return the specified partition array crc32 checksum. sub partition_array_crc ($$$) { my ($pri_or_backup, $n_pe, $in) = @_; local *F; open F, '<', $in or die "$ME: failed to open $in: $!\n"; # Seek to start of partition array. my $off = partition_array_start_offset $pri_or_backup, $n_pe; sysseek (F, $off, 0) or die "$ME: $in: failed to seek to $off: $!\n"; # Read the array. my $p; my $pe_buf; my $n = $n_pe * $pe_size; ($p = sysread F, $pe_buf, $n) && $p == $n or die "$ME: $in: failed to read $pri_or_backup partition array:($p:$n) $!\n"; return crc32 $pe_buf; } # Verify the initial CRC of BUF. sub check_GPT_header ($$$) { my ($pri_or_backup, $in, $buf) = @_; my $curr = curr_LBA $buf; my $backup = backup_LBA $buf; ($pri_or_backup eq 'primary') == ($curr == 1) or die "$ME: $in: invalid curr_LBA($curr) in $pri_or_backup header\n"; ($pri_or_backup eq 'primary') == (34 < $backup) or die "$ME: $in: invalid backup_LBA($backup) in $pri_or_backup header\n"; $pri_or_backup eq 'backup' && $backup != 1 and die "$ME: $in: the backup_LBA in the backup header must be 1\n"; # A primary partition's "partition entries starting LBA" must be 2. if ($pri_or_backup eq 'primary') { my $p = pe_start_LBA $buf; $p == 2 or die "$ME: $in: primary header's PE start LBA is $p (should be 2)\n"; } # Save a copy of the CRC, then zero that field, bytes 16..19: my $orig_crc = unpack ('L<', substr ($buf, 16, 4)); substr ($buf, 16, 4) = "\0" x 4; # Compute CRC32 of header: it'd better match. my $crc = crc32($buf); $orig_crc == $crc or die "$ME: $in: cannot reproduce $pri_or_backup GPT header's CRC32\n"; } # Poke the $N_PE value into $$BUF's number-of-partition-entries slot. sub poke_n_pe ($$) { my ($buf, $n_pe) = @_; # Poke the little-endian value into place. substr ($$buf, 80, 4) = pack ('L<', $n_pe); } # Compute/set partition-array CRC (given $N_PE), then compute a new # header-CRC and poke it into its position, too. sub set_CRCs ($$$$) { my ($pri_or_backup, $buf, $in, $n_pe) = @_; # Compute CRC of primary partition array and put it in substr ($pri, 88, 4) my $pa_crc = partition_array_crc $pri_or_backup, $n_pe, $in; substr ($$buf, 88, 4) = pack ('L<', $pa_crc); # In the backup header, we must also set the 8-byte "Partition entries # starting LBA number" field to reflect our new value of $n_pe. if ($pri_or_backup eq 'backup') { my $off = partition_array_start_offset $pri_or_backup, $n_pe; $off % $ss == 0 or die "$ME: internal error: starting LBA byte offset($off) is" . " not a multiple of $ss\n"; my $lba = $off / $ss; substr ($$buf, 72, 8) = pack ('Q<', $lba); } # Before we compute the checksum, we must zero-out the 4-byte # slot into which we'll store the result. substr ($$buf, 16, 4) = "\0" x 4; my $crc = crc32($$buf); substr ($$buf, 16, 4) = pack ('L<', $crc); } sub usage ($) { my ($exit_code) = @_; my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR); if ($exit_code != 0) { print $STREAM "Try `$ME --help' for more information.\n"; } else { print $STREAM < \$n_partition_entries, 'sector-size=i' => \$ss, help => sub { usage 0 }, version => sub { print "$ME version $VERSION\n"; exit }, ) or usage 1; defined $n_partition_entries or (warn "$ME: --n-partition-array-entries=N not specified\n"), usage 1; defined $ss or $ss = 512; # Require sensible number: # It must either be <= 128, or else a multiple of 4 so that at 128 bytes each, # this array fully occupies a whole number of 512-byte sectors. 1 <= $n_partition_entries && ($n_partition_entries <= 128 || $n_partition_entries % 4 == 0) or die "$ME: invalid number of partition entries: $n_partition_entries\n"; @ARGV == 1 or (warn "$ME: no file specified\n"), usage 1; my $in = $ARGV[0]; local *F; open F, '<', $in or die "$ME: failed to open $in: $!\n"; # Get length and perform some basic sanity checks. my $len = sysseek (F, 0, 2); defined $len or die "$ME: $in: failed to seek to EOF: $!\n"; my $min_n_sectors = 34 + 33 + ($n_partition_entries * $pe_size + $ss - 1) / $ss; my $n_sectors = int (($len + $ss - 1) / $ss); $n_sectors < $min_n_sectors and die "$ME: $in: image file is too small to contain a GPT image\n"; $len % $ss == 0 or die "$ME: $in: size is not a multiple of $ss: $!\n"; # Skip 1st sector. sysseek (F, $ss, 0) or die "$ME: $in: failed to seek to byte $ss: $!\n"; # Read the primary GPT header. my $p; my $pri; ($p = sysread F, $pri, $gpt_header_len) && $p == $gpt_header_len or die "$ME: $in: failed to read the primary GPT header: $!\n"; $backup_LBA = unpack ('Q<', substr ($pri, 32, 8)); # Seek-to and read the backup GPT header. sysseek (F, $backup_LBA * $ss, 0) or die "$ME: $in: failed to seek to backup LBA $backup_LBA: $!\n"; my $backup; ($p = sysread F, $backup, $gpt_header_len) && $p == $gpt_header_len or die "$ME: $in: read failed: $!\n"; close F; check_GPT_header ('primary', $in, $pri); check_GPT_header ('backup', $in, $backup); poke_n_pe (\$pri, $n_partition_entries); poke_n_pe (\$backup, $n_partition_entries); # set both PE CRC and header CRCs: set_CRCs 'primary', \$pri, $in, $n_partition_entries; set_CRCs 'backup', \$backup, $in, $n_partition_entries; # Write both headers back to the file: open F, '+<', $in or die "$ME: failed to open $in: $!\n"; sysseek (F, $ss, 0) or die "$ME: $in: failed to seek to byte $ss: $!\n"; syswrite F, $pri or die "$ME: $in: failed to write primary header: $!\n"; sysseek (F, $backup_LBA * $ss, 0) or die "$ME: $in: failed to seek to backup LBA $backup_LBA: $!\n"; syswrite F, $backup or die "$ME: $in: failed to write backup header: $!\n"; close F or die "$ME: failed to close $in: $!\n"; }