summaryrefslogtreecommitdiff
path: root/tests/gpt-header-munge
blob: 5c0dd80eca9d6f0c4dd143b4929c4967c732c385 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
#!/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 <<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];
  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";
  }