--- /dev/null
+#!/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) = @_;
+ 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";
+ close F;
+
+ 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 <<EOF;
+Usage: $ME [OPTIONS] FILE
+Change the number of GPT partition array entries in FILE.
+
+This option must be specified:
+
+ --n-partition-array-entries=N FIXME
+
+The following are optional:
+
+ --sector-size=N assume sector size of N bytes (default: 512)
+ --help display this help and exit
+ --version output version information and exit
+
+EXAMPLE:
+
+ dd if=/dev/null of=F bs=1 seek=6MB
+ parted -s F mklabel gpt
+ $ME --n=9 F
+
+EOF
+ }
+ exit $exit_code;
+}
+
+{
+ my $n_partition_entries;
+
+ use Getopt::Long;
+ GetOptions
+ (
+ 'n-partition-array-entries=i' => \$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];
+ 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";
+ }
+++ /dev/null
-#!/usr/bin/perl -w
-# Read a GPT header, poke "9" into the 4-byte "Number of partition entries" slot
-# compute crc32 of 9 partition table entries and poke that into place,
-# recompute the primary header's CRC32 checksum and poke that into its slot.
-
-# Set up:
-# dev=dev-file
-# dd if=/dev/null of=$dev bs=1 seek=1026MB
-# parted -s -- $dev mklabel gpt
-
-use strict;
-use warnings;
-use Digest::CRC qw(crc32);
-
-(my $ME = $0) =~ s|.*/||;
-
-my $in = 'dev-file';
-my $n_partition_entries = 9;
-
-sub partition_array_crc ($$)
-{
- my ($n_entries, $in) = @_;
- open F, '<', $in
- or die "$ME: failed to open $in: $!\n";
-
- # Skip 1st 512-byte sector.
- sysseek (F, 2*512, 0)
- or die "$ME: $in: failed to seek: $!\n";
-
- # Read the array.
- my $p;
- my $buf;
- my $n = $n_partition_entries * 128;
- ($p = sysread F, $buf, $n) && $p == $n
- or die "$ME: $in: failed to read partition array: $!\n";
- close F;
-
- return crc32 $buf;
-}
-
-
-open F, '<', $in
- or die "$ME: failed to open $in: $!\n";
-
-# Skip 1st 512-byte sector.
-sysseek (F, 512, 0)
- or die "$ME: $in: failed to seek to byte 512: $!\n";
-
-# Read the primary GPT header.
-my $p;
-my $buf;
-($p = sysread F, $buf, 92) && $p == 92
- or die "$ME: $in: read failed: $!\n";
-close F;
-
-print substr ($buf, 0, 8), "\n";
-
-# Zero the CRC32 field, bytes 16..19
-my $orig_crc = unpack ('L', substr ($buf, 16, 4));
-substr ($buf, 16, 4) = "\0" x 4;
-
-# Before proceeding, compute CRC32 of header as a sanity check.
-my $crc = crc32($buf);
-$orig_crc == $crc
- or die "$ME: $in: cannot reproduce GPT header's CRC32\n";
-
-# poke the $n_partition_entries value into place
-substr ($buf, 80, 4) = pack ('L', $n_partition_entries);
-
-# Compute CRC of partition array and put it in substr ($buf, 88, 4)
-my $pa_crc = partition_array_crc $n_partition_entries, $in;
-substr ($buf, 88, 4) = pack ('L', $pa_crc);
-
-$crc = crc32($buf);
-substr ($buf, 16, 4) = pack ('L', $crc);
-
-# Write it back to the file:
-open F, '+<', $in
- or die "$ME: failed to open $in: $!\n";
-sysseek (F, 512, 0)
- or die "$ME: $in: failed to seek to byte 512: $!\n";
-print F $buf
- or die "$ME: $in: write failed: $!\n";
-close F
- or die "$ME: failed to close $in: $!\n";