File Coverage

blib/lib/Sys/Export/ISO9660Hybrid.pm
Criterion Covered Total %
statement 129 155 83.2
branch 16 50 32.0
condition 3 10 30.0
subroutine 19 22 86.3
pod 11 11 100.0
total 178 248 71.7


line stmt bran cond sub pod time code
1             package Sys::Export::ISO9660Hybrid;
2              
3             our $VERSION = '0.005'; # VERSION
4             # ABSTRACT: Write ISO9660 filesystem overlaid on MBR+GPT partition EFI filesystem
5              
6 1     1   569 use v5.26;
  1         3  
7 1     1   4 use warnings;
  1         2  
  1         39  
8 1     1   3 use experimental qw( signatures );
  1         2  
  1         3  
9 1     1   85 use Carp;
  1         2  
  1         76  
10 1     1   4 use Sys::Export qw( isa_hash isa_handle isa_array write_file_extent expand_stat_shorthand round_up_to_multiple S_ISDIR );
  1         1  
  1         5  
11 1     1   4 use Sys::Export::LogAny '$log';
  1         1  
  1         11  
12 1     1   618 use Sys::Export::GPT;
  1         3  
  1         33  
13 1     1   5 use Sys::Export::ISO9660 qw( BOOT_EFI );
  1         2  
  1         38  
14 1     1   4 use Sys::Export::VFAT;
  1         2  
  1         31  
15             use constant {
16 1         62 ISO_SECTOR_SIZE => Sys::Export::ISO9660::LBA_SECTOR_SIZE,
17             GPT_TYPE_ESP => 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B',
18             GPT_TYPE_GRUB => '21686148-6449-6E6F-744E-656564454649', # 'Hah!IdontNeedEFI'
19 1     1   3 };
  1         1  
20 1     1   4 use Exporter 'import';
  1         1  
  1         1508  
21             our @EXPORT_OK= qw( GPT_TYPE_ESP GPT_TYPE_GRUB );
22              
23              
24 2     2 1 170327 sub new($class, @attrs) {
  2         7  
  2         5  
  2         4  
25             my %attrs= @attrs != 1? @attrs
26 2 50       32 : isa_hash $attrs[0]? %{$attrs[0]}
  0 50       0  
    50          
27             : isa_handle $attrs[0]? ( filehandle => $attrs[0] )
28             : ( filename => $attrs[0] );
29 2         29 my $self= bless {
30             iso => Sys::Export::ISO9660->new,
31             esp => Sys::Export::VFAT->new,
32             gpt => Sys::Export::GPT->new(
33             block_size => 4096,
34             partitions => [
35             { type => GPT_TYPE_ESP, name => 'UEFI System Partition' },
36             ],
37             ),
38             dual_files => [],
39             }, $class;
40             # apply other attributes
41 2         11 $self->$_($attrs{$_}) for keys %attrs;
42 2         5 $self;
43             }
44              
45              
46 0 0   0 1 0 sub filename { @_ > 1? ($_[0]{filename}= $_[1]) : $_[0]{filename} }
47 4 100   4 1 13 sub filehandle { @_ > 1? ($_[0]{filehandle}= $_[1]) : $_[0]{filehandle} }
48              
49              
50 3     3 1 3 sub iso($self) { $self->{iso} }
  3         3  
  3         4  
  3         13  
51 3     3 1 4 sub esp($self) { $self->{esp} }
  3         4  
  3         3  
  3         11  
52 4     4 1 501 sub gpt($self) { $self->{gpt} }
  4         6  
  4         4  
  4         45  
53              
54              
55             sub volume_label {
56 0 0   0 1 0 if (@_ > 1) {
57 0         0 $_[0]->iso->volume_label($_[1]);
58 0         0 $_[0]->esp->volume_label($_[1]);
59             }
60 0         0 $_[0]->iso->volume_label;
61             }
62              
63              
64 2     2 1 3 sub mbr_boot_code($self, @v) {
  2         4  
  2         2  
  2         3  
65 2 50       4 if (@v) {
66 0 0       0 my $data= ref $v[0]? $v[0] : \$v[0];
67 0         0 my $sector0= substr($$data, 0, 512);
68 0 0 0     0 carp "boot_code contains nonzero bytes in partition table area"
69             unless length($sector0) <= 446 || substr($sector0, 446, 64) =~ /^\0+\z/;
70 0         0 $self->{mbr_boot_code}= \$sector0;
71             }
72 2         68 $self->{mbr_boot_code};
73             }
74              
75              
76 0     0 1 0 sub partitions { shift->gpt->partitions(@_) }
77              
78              
79 1     1 1 18 sub add($self, $fileinfo) {
  1         2  
  1         1  
  1         2  
80 1 50       3 $fileinfo= { expand_stat_shorthand($fileinfo) }
81             if isa_array $fileinfo;
82 1   50     7 my $is_dir= S_ISDIR($fileinfo->{mode}||0);
83 1 50       2 if ($is_dir) {
84 0         0 $self->iso->add($fileinfo);
85 0         0 return $self->esp->add($fileinfo);
86             } else {
87 1         3 my $vfile= $self->esp->add({ %$fileinfo, device_align => ISO_SECTOR_SIZE });
88             # prevent ISO9660 from assigning a LBA to this file.
89 1         5 my $ifile= $self->iso->add({ %$fileinfo, data => undef, device_offset => -1 });
90 1         3 push @{$self->{dual_files}}, [ $vfile, $ifile ];
  1         3  
91 1         4 return $vfile;
92             }
93             }
94              
95              
96 2     2 1 17 sub finish($self) {
  2         5  
  2         2  
97 2         9 my $fh= $self->filehandle;
98 2 50       9 if (!$fh) {
99 0 0       0 defined $self->filename or croak "Must set filename or filehandle attributes";
100 0 0       0 open $fh, '+>', $self->filename
101             or croak "open: $!";
102             }
103 2         14 my ($iso, $esp, $gpt4k)= ($self->iso, $self->esp, $self->gpt);
104 2         9 $iso->filehandle($fh);
105 2         10 $esp->filehandle($fh);
106             # Add the El Torrito ESP entry, but we don't know the offset yet
107             # This lets ISO9660 know that it needs to reserve a boot catalog.
108 2         12 my $esp_catalog_entry= $iso->add_boot_catalog_entry(
109             platform => BOOT_EFI,
110             device_offset => 0,
111             size => 0,
112             );
113             # Choose LBA extents for all the directory structures and files other than the shared ones
114             # we marked as device_offset => -1
115 2         14 $iso->allocate_extents;
116             # Now we know how much space the ISO structures and filesystem occupy, and can place all
117             # partitions after that.
118 2         6 my $ofs= $iso->volume_size;
119             # ISO9660 leaves the first 16 2K blocks empty. We need to fit a GPT512 header @512, a GPT4K
120             # header @4K, and a partition table for each from the remaining 24K, so 12K each, 24 entries
121             # with the default entry_size.
122 2         9 my $gpt512= Sys::Export::GPT->new(
123             %$gpt4k,
124             block_size => 512,
125             partitions => [ map +{ %$_, block_size => 512 }, $gpt4k->partitions->@* ]
126             );
127 2         8 my $table_bytes_needed= $gpt4k->entry_size * $gpt4k->partitions->@*;
128 2 50       4 if ($table_bytes_needed <= 3*1024) {
    0          
129 2         5 $gpt512->entry_table_lba(2); # block immediately after the header, ofs=1K
130 2         3 $gpt4k->entry_table_lba(2); # block immediately after the header, ofs=8K
131             } elsif ($table_bytes_needed <= 12*1024) {
132 0         0 $gpt512->entry_table_lba(16); # block following 4K header, ofs=8K
133 0         0 $gpt4k->entry_table_lba(5); # block following 512table, ofs=20K
134             } else { # no room, so tables need to go after the ISO data
135 0         0 $gpt512->entry_table_lba($ofs / 512);
136 0         0 $ofs= round_up_to_multiple($ofs + $table_bytes_needed, 4096);
137 0         0 $gpt4k->entry_table_lba($ofs / 4096);
138 0         0 $ofs+= $table_bytes_needed;
139             }
140              
141             # Now choose locations for all the partitions
142 2         13 $ofs= round_up_to_multiple($ofs, 4096);
143 2         10 $gpt512->first_block($ofs/512);
144 2         6 $gpt4k->first_block($ofs/4096);
145 2         4 for my $i (0 .. $#{$gpt4k->partitions}) {
  2         3  
146 2         4 my $p= $gpt4k->partitions->[$i];
147 2   50     5 $ofs= round_up_to_multiple($ofs, $gpt4k->partition_align // 4096);
148             # This algorithm doesn't currently account for partitons that the user
149             # placed manually.
150 2 50       15 croak "partion $i already has start_lba defined" if defined $p->start_lba;
151 2         5 $p->device_offset($ofs);
152 2 50 33     5 if ($p->type eq GPT_TYPE_ESP && !$esp->volume_offset) {
    0          
153             # Now we know the device offset for the partition containing VFAT
154 2         4 $esp->volume_offset($p->device_offset);
155             # Now we can write the VFAT and get device addresses for all its files
156 2         13 $esp->finish;
157 2         6 $p->size(round_up_to_multiple($esp->geometry->total_size, 4096));
158 2         5 $esp_catalog_entry->{extent}->device_offset($p->device_offset);
159 2         5 $esp_catalog_entry->{extent}->size($p->size);
160             } elsif (!$p->size) {
161             # was data supplied?
162 0 0       0 if ($p->data) {
163 0         0 $p->size(round_up_to_multiple(length ${$p->data}, 4096));
  0         0  
164             } else {
165 0 0       0 croak "Partiton $i lacks size" unless $p->size;
166             }
167             }
168 2         5 $gpt512->partitions->[$i]->device_offset($p->device_offset);
169 2         4 $gpt512->partitions->[$i]->size($p->size);
170 2         10 $ofs= $p->device_offset + $p->size;
171             }
172 2         7 $ofs= round_up_to_multiple($ofs, 4096);
173             # Is the file already sized larger than the amount of remaining space we need?
174 2         5 my $table_size_in_4k= round_up_to_multiple($table_bytes_needed, 4096);
175 2         4 my $need_size= $ofs + $table_size_in_4k * 2 + 4096;
176 2         22 my $size= -s $fh;
177 2 50       5 if ($size < $need_size) {
178 2 50       60 truncate($fh, $need_size) || croak "Can't resize file";
179 2         30 $size= $need_size;
180             }
181             # work backward from end of device
182 2         12 $gpt512->backup_header_lba(int($size / 512) - 1);
183 2         6 $gpt4k->backup_header_lba(int($size / 4096) - 1);
184 2         4 $ofs= $gpt4k->backup_header_lba * 4096 - $table_size_in_4k;
185 2         5 $gpt4k->backup_table_lba($ofs / 4096);
186 2         2 $ofs -= $table_size_in_4k;
187 2         5 $gpt512->backup_table_lba($ofs / 512);
188 2         5 $gpt512->last_block($ofs / 512 - 1);
189 2         5 $gpt4k->last_block($ofs / 4096 - 1);
190              
191             # Now point all the ISO and VFAT files to the same extents
192 2         7 for ($self->{dual_files}->@*) {
193 1         2 my ($vfile, $ifile)= @$_;
194 1 50       4 die "BUG: unaligned file" if $vfile->device_offset % ISO_SECTOR_SIZE;
195 1         3 $ifile->size($vfile->size);
196 1         2 $ifile->device_offset($vfile->device_offset);
197             }
198             # Now we can write the ISO9660
199 2         13 $iso->finish;
200             # Now write the partition tables. Write the 4K one first because the 512 will overwrite
201             # the tail of 4k's backup header.
202 2         8 $gpt4k->write_to_file($fh);
203 2         8 $gpt512->write_to_file($fh);
204             # Last, write the 440 bytes of boot loader if supplied by the user
205 2 50       7 if ($self->mbr_boot_code) {
206 0           write_file_extent($fh, 0, 440, $self->mbr_boot_code, 0, 'Boot Code');
207             }
208             }
209              
210             # Avoiding dependency on namespace::clean
211             delete @{Sys::Export::ISO9660Hybrid::}{qw(
212             carp croak confess write_file_extent expand_stat_shorthand round_up_to_multiple
213             isa_hash isa_handle isa_array S_ISDIR BOOT_EFI
214             )};
215             1;
216              
217             __END__