File Coverage

blib/lib/Sys/Export/ISO9660.pm
Criterion Covered Total %
statement 466 552 84.4
branch 127 218 58.2
condition 99 237 41.7
subroutine 62 68 91.1
pod 23 23 100.0
total 777 1098 70.7


line stmt bran cond sub pod time code
1             package Sys::Export::ISO9660;
2              
3             our $VERSION= 0; our $VERSION = '0.005'; # VERSION
4             # ABSTRACT: Write ISO9660 filesystems with Joliet filename and El Torrito boot catalog support
5              
6              
7 2     2   207807 use v5.26;
  2         6  
8 2     2   8 use warnings;
  2         3  
  2         89  
9 2     2   10 use experimental qw( signatures );
  2         4  
  2         9  
10 2     2   199 use Fcntl qw( S_IFDIR S_ISDIR S_ISREG SEEK_SET SEEK_END );
  2         2  
  2         132  
11 2     2   10 use Scalar::Util qw( blessed dualvar refaddr weaken );
  2         5  
  2         144  
12 2     2   11 use List::Util qw( min max sum );
  2         3  
  2         112  
13 2     2   7 use Time::HiRes 'time';
  2         3  
  2         15  
14 2     2   103 use POSIX 'ceil';
  2         8  
  2         14  
15 2     2   615 use Sys::Export qw( isa_hash isa_handle isa_array write_file_extent expand_stat_shorthand );
  2         25  
  2         12  
16 2     2   10 use Sys::Export::LogAny '$log';
  2         2  
  2         11  
17 2     2   1233 use Encode qw( encode decode );
  2         25194  
  2         351  
18             use constant {
19 2         525 FLAG_HIDDEN => dualvar(0x01, 'FLAG_HIDDEN'), # hidden file
20             FLAG_DIRECTORY => dualvar(0x02, 'FLAG_DIRECTORY'), # directory
21             FLAG_ASSOCIATED => dualvar(0x04, 'FLAG_ASSOCIATED'), # associated file
22             FLAG_RECORD => dualvar(0x08, 'FLAG_RECORD'), # record format
23             FLAG_PROTECTION => dualvar(0x10, 'FLAG_PROTECTION'), # permissions
24             FLAG_MULTIEXTENT => dualvar(0x80, 'FLAG_MULTIEXTENT'), # continued in another extent
25             LBA_SECTOR_SIZE => 2048,
26             LBA_SECTOR_POW2 => 11,
27             BOOT_X86 => dualvar( 0, 'x86'),
28             BOOT_PPC => dualvar( 1, 'PowerPC'),
29             BOOT_MAC => dualvar( 2, 'Mac'),
30             BOOT_EFI => dualvar(0xEF, 'EFI'),
31             EMU_NONE => dualvar(0, 'EMU_NONE'),
32             EMU_FLOPPY12 => dualvar(1, 'EMU_FLOPPY12'),
33             EMU_FLOPPY144 => dualvar(2, 'EMU_FLOPPY144'),
34             EMU_FLOPPY288 => dualvar(3, 'EMU_FLOPPY288'),
35             EMU_HDD => dualvar(4, 'EMU_HDD'),
36 2     2   13 };
  2         2  
37 20     20   18 sub _sector_of($addr) { $addr >> LBA_SECTOR_POW2 }
  20         18  
  20         20  
  20         45  
38 0     0   0 sub _remaining_sector_bytes($pos) { -$pos & (LBA_SECTOR_SIZE-1) }
  0         0  
  0         0  
  0         0  
39 65     65   61 sub _round_to_whole_sector($len) { ($len + LBA_SECTOR_SIZE - 1) & ~(LBA_SECTOR_SIZE-1) }
  65         60  
  65         55  
  65         138  
40             require Sys::Export::ISO9660::File;
41             require Sys::Export::ISO9660::Directory;
42 2     2   9 use Carp;
  2         4  
  2         127  
43             our @CARP_NOT= qw( Sys::Export Sys::Export::Unix );
44 2     2   30 use Exporter 'import';
  2         2  
  2         16214  
45             our @EXPORT_OK= qw(
46             FLAG_HIDDEN FLAG_DIRECTORY FLAG_ASSOCIATED FLAG_RECORD FLAG_PROTECTION FLAG_MULTIEXTENT
47             is_valid_shortname is_valid_joliet_name remove_invalid_shortname_chars
48             LBA_SECTOR_SIZE LBA_SECTOR_POW2 BOOT_X86 BOOT_PPC BOOT_MAC BOOT_EFI
49             EMU_NONE EMU_FLOPPY12 EMU_FLOPPY144 EMU_FLOPPY288 EMU_HDD
50             );
51              
52              
53 5     5 1 7527 sub new($class, @attrs) {
  5         14  
  5         13  
  5         8  
54             my %attrs= @attrs != 1? @attrs
55 5 50       48 : isa_hash $attrs[0]? %{$attrs[0]}
  0 50       0  
    100          
56             : isa_handle $attrs[0]? ( filehandle => $attrs[0] )
57             : ( filename => $attrs[0] );
58 5         64 my $self= bless {
59             root => Sys::Export::ISO9660::Directory->new(name => '/'),
60             # Sectors 0-15 are reserved
61             # + one sector for Primary Volume Descriptor
62             # + one sector for Secondary Volume Descriptor
63             # + one sector for boot catalog descriptor, if used
64             # + Volume Descriptor Set Terminator
65             _free_invlist => [ 19 ], # add one when boot_catalog initialized
66             }, $class;
67             # apply other attributes
68 5         23 $self->$_($attrs{$_}) for keys %attrs;
69 5         29 $self;
70             }
71              
72             # Create dir, and store a strong reference in ->{_subdirs}
73 0     0   0 sub _new_dir($self, $name, $parent, $file) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
74 0         0 my $dir= Sys::Export::ISO9660::Directory->new(name => $name, parent => $parent, file => $file);
75 0         0 $self->{_subdirs}{refaddr $dir}= $dir;
76 0         0 $dir;
77             }
78 12     12   13 sub _new_file($self, $name, %attrs) {
  12         13  
  12         12  
  12         30  
  12         12  
79 12         69 Sys::Export::ISO9660::File->new(name => $name, %attrs);
80             }
81              
82              
83 3 100   3 1 14 sub filename { @_ > 1? ($_[0]{filename}= $_[1]) : $_[0]{filename} }
84 14 100   14 1 64 sub filehandle { @_ > 1? ($_[0]{filehandle}= $_[1]) : $_[0]{filehandle} }
85              
86              
87 58     58 1 202 sub root { $_[0]{root} }
88              
89              
90             sub volume_label {
91             @_ > 1? ($_[0]{volume_label}= _validate_volume_label($_[1])) : $_[0]{volume_label}
92 11 100   11 1 138 }
93 1     1   2 sub _validate_volume_label($label) {
  1         2  
  1         14  
94 1         5 $label= uc $label;
95 1 50       11 croak "Volume label must be 0..32 uppercase letters, digits, underscore, or space characters"
96             unless $label =~ /^[A-Z0-9_ ]{0,32}\z/;
97 1         4 $label;
98             }
99              
100             sub system {
101             @_ > 1? ($_[0]{system}= _validate_system_id($_[1])) : $_[0]{system}
102 10 50   10 1 66 }
103 0     0   0 sub _validate_system_id($label) {
  0         0  
  0         0  
104 0         0 $label= uc $label;
105 0 0       0 croak "System ID must be 0..32 uppercase ASCII characters (excluding several punctuation)"
106             unless $label =~ m{^ [- !"%&'()*+,./0-9:;<=>?A-Z_]{0,32} \z}x;
107 0         0 $label;
108             }
109              
110             sub volume_set {
111             @_ > 1? ($_[0]{volume_set}= _validate_volume_set_id($_[1])) : $_[0]{volume_set}
112 10 50   10 1 25 }
113 0     0   0 sub _validate_volume_set_id($label) {
  0         0  
  0         0  
114 0         0 $label= uc $label;
115 0 0       0 croak "Volume Set ID must be 0..64 uppercase letters, digits, underscore, or space characters"
116             unless $label =~ /^[A-Z0-9_ ]{0,64}\z/; # 128, but Joliet encodes as UTF16
117 0         0 $label;
118             }
119              
120             for (qw( publisher preparer application )) {
121 10 50   10 1 27 eval "sub $_ { \@_ > 1? (\$_[0]{$_}= _validate_meta_label(\$_[1], '$_')) : \$_[0]{$_} } 1" or die "$@";
  10 50   10 1 32  
  10 50   10 1 37  
122             }
123 0     0   0 sub _validate_meta_label($x, $field='Metadata label') {
  0         0  
  0         0  
  0         0  
124             # Metadata labels can either be a text label of the "a-characters" (most of ascii)
125             # or a file object to refer to a file containing the data.
126 0 0 0     0 unless (blessed $x && $x->can('device_offset')) {
127 0         0 $x= uc $x;
128 0 0       0 croak "$field must be 0..64 uppercase ASCII characters (excluding some punctuation)"
129             unless $x =~ m{^ [- !"%&'()*+,./0-9:;<=>?A-Z_]{0,64} \z}x;
130             }
131 0         0 $x;
132             }
133              
134             for (qw( copyright_file abstract_file bibliographic_file )) {
135 11 100   11 1 85 eval "sub $_ { \@_ > 1? (\$_[0]{$_}= _validate_meta_filename(\$_[1], '$_')) : \$_[0]{$_} } 1" or die "$@";
  10 50   10 1 28  
  10 50   10 1 32  
136             }
137 1     1   2 sub _validate_meta_filename($x, $field='Metadata filename') {
  1         2  
  1         2  
  1         1  
138 1 50 33     8 unless (blessed $x && $x->can('device_offset')) {
139 0         0 $x= uc $x;
140 0 0       0 croak "$field must be a valid 8.3 filename"
141             unless $x =~ m{^ [A-Z0-9_]{1,8} (\.[A-Z0-9_]{0,3})? (;[0-9]+)? \z}x;
142             }
143 1         7 $x;
144             }
145              
146              
147 26     26 1 75 sub boot_catalog { $_[0]{boot_catalog} }
148              
149              
150 4     4 1 1045 sub add_boot_catalog_entry($self, %attrs) {
  4         8  
  4         14  
  4         5  
151             my ($platform, $section_id, $bootable, $media_type, $load_segment, $system_type)
152 4         19 = delete @attrs{qw( platform section_id bootable media_type load_segment system_type )};
153             my ($extent, $device_offset, $size, $data)
154 4         12 = delete @attrs{qw( extent device_offset size data )};
155 4 50       8 croak "Unknown attribute ".join(', ', keys %attrs)
156             if keys %attrs;
157 4 50       9 croak "require platform"
158             unless defined $platform;
159              
160 4   33     39 $extent //= Sys::Export::Extent->new(name => "Boot Catalog $platform Boot Image");
161 4 100       13 $extent->device_offset($device_offset) if defined $device_offset;
162 4 100       8 if (defined $data) {
163             # Convert to scalar-ref if scalar
164 1 50       3 $data= do { my $x= $data; \$x } unless ref $data;
  1         2  
  1         2  
165 1   33     8 $size //= length $$data;
166             }
167 4 100       16 $extent->size($size) if defined $size;
168              
169 4   50     23 $bootable //= 0x88; # bootable by default
170 4 50 50     18 $media_type //= $platform == BOOT_EFI? EMU_NONE
    100 33        
171             # If boot loader is larger than a floppy, probably doesn't rely on emulation
172             : ($extent->size//0) > 1474560? EMU_NONE
173             # standard floppy emulation
174             : EMU_FLOPPY144;
175             # load segment is the memory address BIOS loads the image into before running the boot loader
176 4 100 33     15 $load_segment //= $media_type == EMU_NONE? 0 : 0x7C0;
177             # System type for floppy isn't relevant, and HDD emulation would have its own MBR with
178             # the system types defined.
179 4 100 33     15 $system_type //= $platform == BOOT_EFI? 0xEF : 0;
180 4 50 33     30 $section_id //= $platform == BOOT_X86 ? 'x86'
    50          
    50          
    100          
181             : $platform == BOOT_PPC ? 'PowerPC'
182             : $platform == BOOT_MAC ? 'Mac'
183             : $platform == BOOT_EFI ? 'UEFI'
184             : "Platform $platform";
185              
186 4 100       9 if (!$self->{boot_catalog}) {
187             # Ensure sector needed by boot catalog descriptor wasn't already reserved,
188             # e.g. adding boot entries after calling allocate_extents
189             # This logic needs enhanced if other descriptors become optional.
190             croak "Cannot add boot_catalog after allocating sector 19"
191 3 50       8 unless $self->{_free_invlist}[0] == 19;
192 3         10 $self->{_free_invlist}[0]++;
193             }
194              
195 4   100     26 my $sections= $self->{boot_catalog}{sections} //= [];
196 4   33     17 my ($sec)= grep $_->{platform} == $platform && $_->{id_string} eq $section_id, @$sections;
197 4 50       12 if (!defined $sec) {
198 4         14 push @$sections, ($sec= {
199             platform => $platform,
200             id_string => $section_id,
201             entries => []
202             });
203             }
204             push $sec->{entries}->@*, {
205 4         20 bootable => $bootable,
206             media_type => $media_type,
207             load_segment => $load_segment,
208             system_type => $system_type,
209             extent => $extent,
210             };
211 4         29 return $sec->{entries}[-1];
212             }
213              
214              
215             sub default_time {
216             @_ > 1? ($_[0]{default_time}= $_[1]) : $_[0]{default_time}
217 1 50   1 1 7 }
218              
219              
220 9     9 1 777 sub volume_size($self) { $self->{_free_invlist}[-1] * 2048 }
  9         13  
  9         11  
  9         24  
221              
222              
223 8     8 1 169543 sub is_valid_shortname($name) {
  8         29  
  8         9  
224 8         90 !!($name =~ /^[A-Z0-9_~]{1,8} (\. [A-Z0-9_]{1,3} )? \z/x)
225             }
226              
227 11     11 1 3741 sub is_valid_joliet_name($name) {
  11         30  
  11         24  
228 11         66 !!($name =~ m{^[^\x00-\x1F\\/;\x7F-\x9F]{1,127}\z}x)
229             }
230              
231 0     0 1 0 sub remove_invalid_shortname_chars($name, $replacement='_') {
  0         0  
  0         0  
  0         0  
232 0         0 $name =~ tr/a-z/A-Z/; # perform 'uc' but only for the ASCII range
233 0         0 $name =~ s/[^A-Z0-9_]+/$replacement/gr;
234             }
235              
236              
237 2     2 1 11 sub add($self, $spec) {
  2         24  
  2         4  
  2         3  
238 2 100       13 $spec= { expand_stat_shorthand($spec) }
239             if isa_array $spec;
240              
241             defined $spec->{uname} or defined $spec->{name}
242 2 50 33     16 or croak "Require 'uname' or 'name'";
243 2 50       6 defined $spec->{mode} or croak "Require 'mode'";
244              
245             # If user supplied uname, use that as a more official source of Unicode
246 2   33     20 my $path= $spec->{uname} // decode('UTF-8', $spec->{name}, Encode::FB_CROAK | Encode::LEAVE_SRC);
247 2         80 $path =~ s,^/,,; # remove leading slash, if any
248              
249 2 50       7 croak "ISO9660 has a max path length of 255 (253 with ';1' version suffix), at '$path'"
250             if length($path) > 253;
251 2         14 my @path= grep length, split '/', $path;
252 2         4 my $leaf= pop @path;
253 2 50       4 croak "ISO9660 has a max subdirectory depth of 7, at '$path'"
254             if @path > 7; # It's documented as 8, but includes the root directory
255              
256             # Walk through the tree based on the case-folded path
257 2         6 my $parent= $self->root;
258 2         11 for (@path) {
259 0         0 my $ent= $parent->entry($_);
260 0 0       0 if ($ent) {
261             croak $ent->name." is not a directory, while attempting to add '$path'"
262 0 0       0 unless $ent->{dir};
263             } else { # Auto-create directory. Autovivication is indicated by ->{file} = undef
264 0         0 $ent= $parent->add($_, undef);
265 0 0       0 my $name= ($parent == $self->root? '' : $parent->name) . "/$_";
266 0         0 weaken($ent->{dir}= $self->_new_dir($name, $parent, undef));
267             }
268 0         0 $parent= $ent->{dir};
269             }
270              
271 2         4 my $flags= $spec->{ISO9660_flags};
272 2         3 my $file;
273 2 50       9 if (S_ISREG($spec->{mode})) {
    0          
274             my ($size, $offset, $data_ref)
275 2         3 = @{$spec}{qw( size device_offset data )};
  2         5  
276 2 50 66     10 $data_ref= do { my $x= $data_ref; \$x }
  0         0  
  0         0  
277             if defined $data_ref && !ref $data_ref;
278 2 50       11 if ($size) {
    100          
279             # ensure data matches
280 0 0 0     0 croak "File $path ->{data} length disagrees with ->{size}"
281             if $data_ref && length($$data_ref) != $size;
282             } elsif (defined $data_ref) {
283 1   33     4 $size //= length($$data_ref);
284             }
285             $file= $self->_new_file("/$path", size => $size, flags => $flags, mtime => $spec->{mtime},
286 2         12 device_offset => $offset, data => $data_ref);
287             } elsif (S_ISDIR($spec->{mode})) {
288 0         0 $flags |= FLAG_DIRECTORY;
289             # If adding this directory overtop a previous auto-vivified directory, the ->{file}
290             # will be empty and we can just update it.
291 0         0 my $cur= $parent->entry($leaf);
292             croak "Attempt to add duplicate directory $leaf"
293 0 0 0     0 if $cur && $cur->{file};
294 0         0 $file= $self->_new_file("/$path", size => 0, flags => $flags, mtime => $spec->{mtime});
295 0 0       0 if ($cur) {
296 0         0 $cur->{file}= $file;
297 0         0 $cur->{dir}{file}= $file;
298 0         0 $log->debugf("updated attributes of %s", $path);
299 0         0 return $file;
300             }
301             # otherwise, add this file to a directory entry
302             }
303             else {
304             # TODO: add conditional symlink support via hardlinks
305 0         0 croak "Can only export files or directories into VFAT"
306             }
307              
308             # this also checks for name collisions on shortname
309 2         20 my $ent= $parent->add($leaf, $file, shortname => $spec->{ISO9660_shortname});
310             # If the dirent is a directory, also add a directory object to the dirent
311 2 50       8 if ($file->is_dir) {
312             # the directory object also gets a reference to its file object.
313 0         0 weaken($ent->{dir}= $self->_new_dir("/$path", $parent, $file));
314             }
315              
316             $log->debugf("added %s longname=%s shortname=%s %s",
317             $path, $ent->{name}, $ent->{shortname}, (
318             !$ent->{file}? 'size=0 (empty file)'
319             : $ent->{file}->is_dir? 'DIR'
320             : sprintf("size=0x%X device_offset=0x%X",
321 2 0 0     14 $ent->{file}->size, $ent->{file}->device_offset//0)
    0          
    50          
322             ))
323             if $log->is_debug;
324              
325 2         22 $file;
326             }
327              
328              
329 5     5 1 20 sub finish($self) {
  5         6  
  5         6  
330 5   66     32 $self->{default_time} //= time;
331             # Choose LBA extents for everything
332 5         17 $self->allocate_extents;
333              
334 5         35 my $fh= $self->filehandle;
335 5 100       22 if (!$fh) {
336 1 50       3 defined $self->filename or croak "Must set filename or filehandle attributes";
337 1 50       5 open $fh, '+>', $self->filename
338             or croak "open: $!";
339             }
340             # check size / truncate larger
341 5         110 my $size= $self->volume_size;
342 5 100       73 if (-s $fh < $size) {
343 3 50       91 truncate($fh, $size) or croak "truncate(iso, $size): $!";
344             }
345 5         21 $self->_write_filesystem($fh);
346 5 100       12 unless ($self->filehandle) {
347 1 50       26 $fh->close or croak "close: $!";
348             }
349 5         369 1;
350             }
351              
352             # Write a ISO9660::File object to its configured extent, and clear its ->{data}
353 39     39   41 sub _write_file($fh, $file) {
  39         34  
  39         38  
  39         37  
354 39 50 33     57 defined $file->size || defined $file->data or die "BUG: file $file->{name} has no size or data";
355 39   33     61 my $size= _round_to_whole_sector($file->size // length ${$file->data});
  0         0  
356 39 100 66     93 write_file_extent($fh, $file->device_offset, $size, $file->data, 0, $file->name)
357             if $size && defined $file->data;
358 39         81 $file->data(undef); # free up memory as we go, also deallocates mmaps
359             }
360              
361             # Write all descriptors, path tables, directories, and any file with ->{data}.
362             # Note that _write_file removes ->data form the file as it is written.
363 5     5   7 sub _write_filesystem($self, $fh) {
  5         7  
  5         7  
  5         6  
364 5         11 my $boot_catalog= $self->boot_catalog;
365 5         6 my $lba= 16;
366             # Write Primary Volume Descriptor
367 5         30 write_file_extent($fh, $lba++ * LBA_SECTOR_SIZE, LBA_SECTOR_SIZE,
368             \$self->_pack_primary_volume_descriptor, 0, 'Primary Volume Descriptor');
369             # Write Secondary Volume Descriptor (Joliet)
370 5         22 write_file_extent($fh, $lba++ * LBA_SECTOR_SIZE, LBA_SECTOR_SIZE,
371             \$self->_pack_joliet_volume_descriptor, 0, 'Joliet Volume Descriptor');
372             # Write boot catalog if present
373 5 100       23 write_file_extent($fh, $lba++ * LBA_SECTOR_SIZE, LBA_SECTOR_SIZE,
374             \$self->_pack_boot_catalog_descriptor, 0, 'Boot Catalog Descriptor')
375             if $boot_catalog;
376             # Volume Set Terminator
377 5         22 write_file_extent($fh, $lba++ * LBA_SECTOR_SIZE, LBA_SECTOR_SIZE,
378             \"\xFFCD001\x01", 0, 'Volume Set Terminator');
379              
380             # Boot catalog
381 5 100       13 if ($boot_catalog) {
382 3         12 $self->_pack_boot_catalog;
383 3         10 _write_file($fh, $boot_catalog->{extent});
384            
385             # Write any supplied data for boot entry
386 3         6 for my $sec ($boot_catalog->{sections}->@*) {
387 4   50     17 $_->{extent} && _write_file($fh, $_->{extent}) for $sec->{entries}->@*;
388             }
389             }
390              
391             # Write path tables
392 5         19 $self->_pack_path_tables;
393 5         20 _write_file($fh, $_) for $self->{path_tables}->@{qw( le be jle jbe )};
394              
395             # Write directory entries
396 0         0 my @dirs= sort { $a->file->device_offset <=> $b->file->device_offset }
397 5         15 $self->root, values $self->{_subdirs}->%*;
398 5         10 for (@dirs) {
399 5         15 $self->_pack_directory($_);
400 5   50     12 $_ && _write_file($fh, $_) for $_->file, $_->joliet_file;
401             }
402              
403             # Add any non-directory files that have data defined
404 5         10 for my $dir (@dirs) {
405 5   50     16 $_->{file} && _write_file($fh, $_->{file}) for $dir->entries->@*;
406             }
407             }
408              
409              
410             sub allocate_extents {
411 8     8 1 124 my $self= shift;
412             my $assign_lba= sub {
413 65   100 65   347 my $sec_size= ceil(($_->size // 0) / LBA_SECTOR_SIZE);
414 65 100 100     147 if ($sec_size && !defined $_->device_offset) {
    100 66        
415             # Allocate sectors for this file. Can't assign via lba attribute because the
416             # boot_catalog entry extents are 512-byte blocks
417 35         56 $_->device_offset($self->_allocate_sectors($sec_size) * LBA_SECTOR_SIZE);
418             } elsif ($sec_size && $_->device_offset > 0) {
419             # make sure these sectors are reserved
420 26         38 $self->_mark_sectors_reserved($_->device_offset >> LBA_SECTOR_POW2, $sec_size);
421             }
422 65         116 $log->debugf("File %s at LBA (%d) %d +%d", $_->name, $_->block_size, $_->start_lba, $sec_size);
423 8         61 };
424              
425 8 100       21 if (my $boot_catalog= $self->boot_catalog) {
426 6         19 $self->_calc_boot_catalog_size;
427 6         16 &$assign_lba for $boot_catalog->{extent};
428 6         125 for my $sec ($boot_catalog->{sections}->@*) {
429 8         42 &$assign_lba for grep defined, map $_->{extent}, $sec->{entries}->@*;
430             }
431             }
432              
433             # Add the 4 path tables here
434 8         68 $self->_calc_dir_sizes;
435 8         63 $self->_calc_path_table_size;
436 8         26 &$assign_lba for $self->{path_tables}->@{qw( le be jle jbe )};
437              
438             # Add the directories here
439 8         52 my @dirs= ( $self->root, values $self->{_subdirs}->%* );
440 8         11 for my $dir (@dirs) {
441 8         20 &$assign_lba for $dir->file, $dir->joliet_file;
442             }
443             # Add any files that didn't get assigned yet
444 8         63 for my $dir (@dirs) {
445 8         16 &$assign_lba for grep defined, map $_->{file}, $dir->entries->@*;
446             }
447 8         77 return $self->{_free_invlist}[-1] * 2048;
448             }
449              
450             # Find an available range of sectors to allocate from the free list
451 35     35   33 sub _allocate_sectors($self, $sector_count) {
  35         34  
  35         30  
  35         42  
452 35         38 my $inv= $self->{_free_invlist};
453             # Iterate the inversion list looking for a range large enough
454             # The end of the list has lim=undef.
455 35         61 for (my $i= 0; $i <= @$inv; $i+= 2) {
456 35         53 my ($start, $lim)= $inv->@[$i,$i+1];
457 35 50 33     71 if (!$lim || $lim - $start > $sector_count) {
458 35         41 $inv->[$i] += $sector_count;
459 35         76 return $start;
460             }
461 0 0       0 if ($lim - $start == $sector_count) {
462 0         0 splice(@$inv, $i, 2);
463 0         0 return $start;
464             }
465             }
466 0         0 die "BUG"; # should never get here, as long as list is odd length.
467             }
468              
469             # Mark a range of sectors as unavailable for allocation
470 26     26   24 sub _mark_sectors_reserved($self, $reserve_lba, $sector_count) {
  26         26  
  26         39  
  26         26  
  26         16  
471 26         33 my $reserve_lim= $reserve_lba + $sector_count;
472 26         28 my $inv= $self->{_free_invlist};
473 26         42 for (my $i= 0; $i < @$inv; $i += 2) {
474 29         40 my ($start, $lim)= $inv->@[$i,$i+1];
475             # If this free range is entirely beyond the $lba+$count, request is already reserved
476 29 100       46 return if $start >= $reserve_lim;
477             # If this free range is entirely less than the request, continue loop
478 4 100 66     49 next if $lim && $lim <= $reserve_lba;
479             # If start == lba, move start forward, possibly consuming it
480 3 100       21 if ($start >= $reserve_lba) {
481 1 50 33     4 if (!$lim || $reserve_lim < $lim) {
482 1         3 $inv->[$i]= $reserve_lim;
483 1         2 return;
484             } else {
485 0         0 splice(@$inv, $i, 2);
486 0 0       0 return if $reserve_lim == $lim;
487 0         0 $i -= 2; # keep looping until free range is beyond reserve_lim
488             }
489             } else { # start < lba, truncate current range
490 2         4 $inv->[$i+1]= $reserve_lba;
491             # end of the loop? add a new open-ended range
492 2 50       21 $inv->[$i+2]= $reserve_lim if $i+1 == $#$inv;
493             }
494             }
495             }
496              
497 8     8   11 sub _calc_dir_sizes($self) {
  8         8  
  8         67  
498             # First, ensure every directory has file and joliet_file and shortname defined
499 8         21 my @dirs= ( $self->root, values $self->{_subdirs}->%* );
500 8         10 for (@dirs) {
501 8   66     53 $_->{file} //= $self->_new_file($_->name, flags => FLAG_DIRECTORY);
502 8   66     31 $_->{joliet_file} //= $self->_new_file($_->name . ' [J]', flags => $_->file->flags);
503             # Need the 8.3 name in order to know whether it matches the long name
504 8         40 $_->build_shortnames;
505             }
506 8         21 for my $dir (@dirs) {
507 8         12 my $size= 34; # '.' entry
508 8 50       26 $size += 34 unless $dir->is_root; # '..' entry
509 8         13 my $joliet= $size;
510 8         12 for ($dir->entries->@*) {
511 3   33     22 my $is_dir= $_->{dir} || ($_->{file} && $_->{file}->is_dir);
512 3         5 my $shortname_len= length $_->{shortname};
513 3         5 my $pos= $size;
514 3 50       23 $size += 33 + (($shortname_len + ($is_dir? 0 : 2))|1);
515 3 50       73 $size += _remaining_sector_bytes($pos) if _sector_of($pos) != _sector_of($size); # round to next sector
516 3         5 $pos= $joliet;
517 3         37 my $joliet_name= encode('UTF-16BE', $_->{name}, Encode::FB_CROAK | Encode::LEAVE_SRC);
518 3         72 $joliet += 33 + (length($joliet_name)|1);
519 3 50       6 $joliet += _remaining_sector_bytes($pos) if _sector_of($pos) != _sector_of($joliet); # round to next sector
520             # directory entries get added to a top-level "path table".
521             # while we have the name available, calculate the size of those, too.
522 3 50       8 if ($is_dir) {
523             # gets padded to even length
524 0         0 $_->{path_table_size}= 8 + $shortname_len + ($shortname_len & 1);
525             # utf-16 will always be even length
526 0         0 $_->{path_table_jsize}= 8 + length($joliet_name);
527             }
528             }
529 8         25 $dir->{file}->size(_round_to_whole_sector($size));
530 8         14 $dir->{joliet_file}->size(_round_to_whole_sector($joliet));
531             }
532             }
533              
534 24     24   23 sub _pack_iso_datetime($epoch) {
  24         23  
  24         23  
535 24         55 my @t = gmtime($epoch);
536             # year since 1900
537 24         23 my $tz = 0; # UTC offset in 15min intervals
538 24         115 return pack('C7', $t[5], $t[4]+1, $t[3], $t[2], $t[1], $t[0], $tz);
539             }
540              
541             our @dirent_fields = (
542             [ dir_len => 0, 1, 'C' ],
543             [ ext_attr_len => 1, 1, 'C', 0 ],
544             [ extent_lba => 2, 4, 'V' ],
545             [ extent_lba => 6, 4, 'N' ],
546             [ size => 10, 4, 'V' ],
547             [ size => 14, 4, 'N' ],
548             [ packed_mtime => 18, 7, 'a7' ],
549             [ flags => 25, 1, 'C', 0 ],
550             [ unit_size => 26, 1, 'C', 0 ],
551             [ gap_size => 27, 1, 'C', 0 ],
552             [ seq => 28, 2, 'v', 1 ], # 'seq' only used for multi-disc sets
553             [ seq => 30, 2, 'n', 1 ], # will always be '1' for a single image.
554             [ name_len => 32, 1, 'C' ],
555             # name_len and name appended after 33+
556             );
557 24     24   26 sub _encode_dirent($self, $name, $file, $ent= {}) {
  24         27  
  24         31  
  24         23  
  24         26  
  24         22  
558 24         24 my $name_len= length $name;
559 24         31 my $dir_len= 33 + ($name_len|1);
560 24   33     75 my $extent_lba= $ent->{extent_lba} // $file && $file->start_lba // 0;
      33        
      50        
561 24   33     68 my $size= $ent->{size} // $file && $file->size // 0;
      33        
      50        
562 24   50     53 my $seq= $ent->{seq} // 1;
563             pack 'C C V N V N a7 C C C v n C a'.($name_len|1),
564             $dir_len, $ent->{ext_attr_len} // 0,
565             $extent_lba, $extent_lba,
566             $size, $size,
567             _pack_iso_datetime($ent->{mtime} // $file && $file->mtime // $self->{default_time}),
568             $ent->{flags} // $file && $file->flags // 0,
569             $ent->{unit_size} // 0,
570 24   50     4479 $ent->{gap_size} // 0,
      33        
      33        
      33        
      66        
      66        
      100        
      50        
      50        
571             $seq, $seq,
572             $name_len, $name;
573             }
574              
575 5     5   6 sub _pack_directory($self, $dir) {
  5         7  
  5         6  
  5         6  
576             # dot and dotdot
577 5         15 my $data= $self->_encode_dirent("\x00", $dir->file);
578 5         14 my $joliet= $self->_encode_dirent("\x00", $dir->joliet_file);
579 5 50       29 if (my $parent= $dir->parent) {
580 0         0 $data .= $self->_encode_dirent("\x01", $parent->file);
581 0         0 $joliet .= $self->_encode_dirent("\x01", $parent->joliet_file);
582             }
583             # real entries
584 5         12 for (sort { lc $a->{name} cmp lc $b->{name} } $dir->entries->@*) {
  0         0  
585 2   33     12 my $is_dir= $_->{dir} || $_->{file}->is_dir;
586 2   50     22 my $shortname= $_->{shortname} . (!$is_dir && ';1');
587 2         6 my $pos= length $data;
588 2         6 $data .= $self->_encode_dirent($shortname, $_->{file}, $_);
589             # If it crossed a sector boundary, move it fully into the next sector
590 2 50       6 substr($data, $pos, 0, "\0"x _remaining_sector_bytes($pos))
591             if _sector_of($pos) != _sector_of(length $data);
592 2         12 my $jname= encode('UTF-16BE', $_->{name}, Encode::FB_CROAK | Encode::LEAVE_SRC);
593             my $jfile= $_->{dir}? $_->{dir}->joliet_file # live dir holds reference to joliet_file
594             : $is_dir? $_->{joliet_file} # dirent could also reference the joliet_file
595 2 50       42 : $_->{file}; # else it's a plain file with no alt encoding
    50          
596 2         2 $pos= length $joliet;
597 2         7 $joliet .= $self->_encode_dirent($jname, $jfile, $_);
598             # If it crossed a sector boundary, move it fully into the next sector
599 2 50       5 substr($joliet, $pos, 0, "\0"x _remaining_sector_bytes($pos))
600             if _sector_of($pos) != _sector_of(length $joliet);
601             }
602             # verify correct size
603 5 50       13 _round_to_whole_sector(length $data) == $dir->file->size
604             or die "BUG: encoded directory is ".length($data)." bytes but should be ".$dir->file->size;
605 5 50       11 _round_to_whole_sector(length $joliet) == $dir->joliet_file->size
606             or die "BUG: encoded joliet directory is ".length($data)." bytes but should be ".$dir->joliet_file->size;
607 5         13 $dir->file->{data}= \$data;
608 5         9 $dir->joliet_file->{data}= \$joliet;
609             }
610              
611             # The path_table encoding algorithm performs a depth-first iteration because the entries refer
612             # to the parent using a index into the table, so the entry needs to already exist.
613             # This depth-first traversal also supports the feature of allowing directories to form an
614             # acyclic graph, in which case one directory could get traversed multiple times; the path table
615             # needs to have every distinct path indexed.
616              
617             sub _calc_path_table_size($self) {
618             my @paths= ( [ $self->root, 0 ] );
619             my $i= 0;
620             my ($size, $jsize)= (10, 10); # start with root dir, always 10 bytes
621             while ($i < @paths) {
622             my ($dir, $parent_id)= @{ $paths[$i++] };
623             for my $ent ($dir->entries->@*) {
624             if ($ent->{dir}) {
625             defined $ent->{path_table_size} && defined $ent->{path_table_jsize}
626             or die join(' ', %$ent);
627             $size += $ent->{path_table_size};
628             $jsize+= $ent->{path_table_jsize};
629             push @paths, [ $ent->{dir}, $i ]; # 1-based index, which works because ++ above
630             }
631             }
632             }
633             my sub path_table_extent($name) {
634             Sys::Export::Extent->new(block_size => 2048, name => $name)
635             }
636             ($self->{path_tables}{le} //= path_table_extent('Path Table [LE]'))->size($size);
637             ($self->{path_tables}{be} //= path_table_extent('Path Table [BE]'))->size($size);
638             ($self->{path_tables}{jle} //= path_table_extent('Path Table [LE,J]'))->size($jsize);
639             ($self->{path_tables}{jbe} //= path_table_extent('Path Table [BE,J]'))->size($jsize);
640             }
641              
642 5     5   8 sub _pack_path_tables($self) {
  5         6  
  5         8  
643             # Need packed for both 8.3 filenames and Joliet filenames, and encoded both little-endian
644             # and big-endian, for 4 total path tables.
645 5         8 my ($le, $be, $jle, $jbe);
646 5         16 $le= $jle= pack 'C C V v a2', 10, 0, $self->root->file->lba, 1, '';
647 5         11 $be= $jbe= pack 'C C N n a2', 10, 0, $self->root->file->lba, 1, '';
648 5         11 my @paths= ( [ $self->root, 1 ] );
649 5         8 my $i= 0;
650 5         8 my ($size, $jsize)= (0, 0);
651 5         13 while ($i < @paths) {
652 5         9 my ($dir, $parent_id)= @{ $paths[$i++] };
  5         9  
653 5         11 for my $ent ($dir->entries->@*) {
654 2 50       10 if ($ent->{dir}) {
655 0         0 my @vals= ( $ent->{path_table_size}, 0, $ent->{dir}->file->lba, $parent_id, $ent->{shortname} );
656 0         0 $le .= pack 'C C V v a'.($ent->{path_table_size}-8), @vals;
657 0         0 $be .= pack 'C C N n a'.($ent->{path_table_size}-8), @vals;
658 0         0 my $jname= encode('UTF-16BE', $ent->{name}, Encode::FB_CROAK | Encode::LEAVE_SRC);
659 0         0 @vals= ( $ent->{path_table_jsize}, 0, $ent->{dir}->joliet_file->lba, $parent_id, $jname );
660 0         0 $jle .= pack 'C C V v a*', @vals;
661 0         0 $jbe .= pack 'C C N n a*', @vals;
662 0         0 push @paths, [ $ent->{dir}, $i ]; # 1-based index, which works because ++ above
663             }
664             }
665             }
666 5         9 my $path_tables= $self->{path_tables};
667             die "BUG: wrong encoded path_table size"
668             unless length $le == $path_tables->{le}->size && length $be == $path_tables->{be}->size
669 5 50 33     15 && length $jle == $path_tables->{jle}->size && length $jbe == $path_tables->{jbe}->size;
      33        
      33        
670 5         17 $path_tables->{le}->data(\$le);
671 5         13 $path_tables->{be}->data(\$be);
672 5         11 $path_tables->{jle}->data(\$jle);
673 5         10 $path_tables->{jbe}->data(\$jbe);
674             }
675              
676 20     20   13 sub _pack_iso_volume_datetime($epoch) {
  20         21  
  20         20  
677 20         40 my @t = gmtime($epoch); # UTC
678 20         38 my $year = $t[5] + 1900;
679 20         21 my $mon = $t[4] + 1;
680 20         20 my $mday = $t[3];
681 20         33 my $hour = $t[2];
682 20         24 my $min = $t[1];
683 20         99 my $sec = $t[0];
684 20         31 my $centi = ($epoch * 100 % 100); # hundredths of seconds
685 20         19 my $tz = 0; # UTC → offset=0, units of 15 min
686 20         82 return sprintf("%04d%02d%02d%02d%02d%02d%02d%c",
687             $year, $mon, $mday, $hour, $min, $sec,
688             $centi, $tz,
689             );
690             }
691              
692             our @vol_desc_fields = (
693             # field_name | offset | size | pack-code | default
694             [ type_code => 0x000, 1, 'C', 1 ],
695             [ std_id => 0x001, 5, 'A', 'CD001' ],
696             [ version => 0x006, 1, 'C', 1 ],
697             #[ unused1 => 0x007, 1, 'C', 0 ],
698             [ system_id => 0x008, 32, 'E', '' ], # system which uses sectors 0-15
699             [ volume_id => 0x028, 32, 'E', '' ],
700             #[ unused2 => 0x048, 8, 'a8', '' ],
701             [ volume_space => 0x050, 4, 'V' ],
702             [ volume_space => 0x054, 4, 'N' ],
703             [ escape_sequences => 0x058, 32, 'a', '' ],
704             [ vol_set_sz => 0x078, 2, 'v', 1 ],
705             [ vol_set_sz => 0x07A, 2, 'n', 1 ],
706             [ seq_no => 0x07C, 2, 'v', 1 ],
707             [ seq_no => 0x07E, 2, 'n', 1 ],
708             [ blk_sz => 0x080, 2, 'v', 2048 ],
709             [ blk_sz => 0x082, 2, 'n', 2048 ],
710             [ path_sz => 0x084, 4, 'V' ],
711             [ path_sz => 0x088, 4, 'N' ],
712             [ path_le => 0x08C, 4, 'V' ],
713             [ path_le_opt => 0x090, 4, 'V', 0 ],
714             [ path_be => 0x094, 4, 'N' ],
715             [ path_be_opt => 0x098, 4, 'N', 0 ],
716             [ root_dirent => 0x09C, 34, 'a' ],
717             [ vol_set_id => 0x0BE, 128, 'E', '' ],
718             [ pub_id => 0x13E, 128, 'E', '' ],
719             [ prep_id => 0x1BE, 128, 'E', '' ],
720             [ app_id => 0x23E, 128, 'E', '' ],
721             [ copy_id => 0x2BE, 37, 'E', '' ],
722             [ abs_id => 0x2E3, 37, 'E', '' ],
723             [ bib_id => 0x308, 37, 'E', '' ],
724             [ creation_ts => 0x32D, 17, 'a', '0'x16 ],
725             [ modification_ts => 0x33E, 17, 'a', '0'x16 ],
726             [ expiration_ts => 0x34F, 17, 'a', '0'x16 ],
727             [ effective_ts => 0x360, 17, 'a', '0'x16 ],
728             [ file_struct => 0x371, 1, 'C', 1 ],
729             #[ unused3 => 0x372, 1, 'C', 0 ],
730             );
731              
732             # Given fields and an offset, add all the pack args to the first 2 params.
733             # The optional '$charset' performs character encoding on any field of type 'E'.
734 21     21   24 sub _append_pack_args($pack, $vals, $ofs, $fields, $attrs, $charset=undef) {
  21         22  
  21         20  
  21         22  
  21         21  
  21         23  
  21         24  
  21         25  
735 21         43 for (@$fields) {
736 392         569 my ($name, $field_ofs, $size, $code, $default)= @$_;
737 392   66     702 my $val= $attrs->{$name} // $default
      33        
738             // croak "No value supplied for $name, and no default";
739 392 100       487 if ($code eq 'E') { # 'E' is a virtual code I'm using to mean "Encoded space-padded string"
740 90 100       108 if ($charset) {
741             # pad with encoded spaces
742 45         180 $val= substr(encode($charset, $val.(' 'x$size), Encode::FB_CROAK), 0, $size);
743             }
744 90         7570 $code= 'A';
745             }
746 392 100       533 if (uc $code eq 'A') {
747 177 50 33     405 utf8::is_utf8($val) or utf8::downgrade($val, 1)
748             or croak "Wide character supplied for attribute $name";
749 177 50       213 carp "$name will be truncated" if length($val) > $size;
750 177         172 $code .= $size;
751             }
752 392         510 push @$pack, '@'.($ofs+$field_ofs).$code;
753 392         546 push @$vals, $val;
754             }
755             }
756              
757             # Helper for packing a single list of fields in one function call
758 21     21   24 sub _pack_fields($field_array, $attrs, $charset=undef) {
  21         21  
  21         18  
  21         25  
  21         19  
759 21         26 my (@pack, @vals);
760 21         54 _append_pack_args(\@pack, \@vals, 0, $field_array, $attrs, $charset);
761 21         328 pack join(' ', @pack), @vals;
762             }
763              
764 5     5   6 sub _pack_primary_volume_descriptor($self, $attrs={}) {
  5         7  
  5         8  
  5         20  
765 5         10 $attrs->{root_file}= $self->root->file;
766 5         14 $attrs->{path_le}= $self->{path_tables}{le};
767 5         10 $attrs->{path_be}= $self->{path_tables}{be};
768 5         15 $self->_pack_volume_descriptor($attrs);
769             }
770              
771 5     5   7 sub _pack_joliet_volume_descriptor($self, $attrs={}) {
  5         7  
  5         8  
  5         7  
772 5         13 $attrs->{type_code}= 2;
773 5         11 $attrs->{escape_sequences} = "%/E"; # Level 3 of Joliet, UCS-2BE
774 5         13 $attrs->{root_file}= $self->root->joliet_file;
775 5         12 $attrs->{path_le}= $self->{path_tables}{jle};
776 5         9 $attrs->{path_be}= $self->{path_tables}{jbe};
777 5         11 $self->_pack_volume_descriptor($attrs);
778             }
779              
780             # Packs a primary or secondar volume descriptor
781 10     10   11 sub _pack_volume_descriptor($self, $attrs) {
  10         12  
  10         11  
  10         9  
782 10   100     47 my $is_joliet= ($attrs->{type_code}//0) == 2;
783             # The pub_id, prep_id, and app_id fields can either be literal text, or they can be a
784             # reference to a file in the root dir by starting with a '_' character.
785 30     30   25 my $maybe_file_ref= sub($thing) {
  30         29  
  30         26  
786 30 50 33     44 return '_'.$self->_get_metadata_filename($thing, $is_joliet)
787             if blessed($thing) && $thing->can('device_offset');
788 30         81 return $thing;
789 10         65 };
790 10   33     75 $attrs->{system_id} //= $self->system // uc($^O);
      33        
791 10   100     46 $attrs->{volume_id} //= $self->volume_label // 'CDROM';
      33        
792 10   33     36 $attrs->{volume_space} //= $self->{_free_invlist}[-1];
793 10   33     46 $attrs->{path_sz} //= $attrs->{path_le}->size;
794 10   33     18 $attrs->{path_le} //= $attrs->{path_le}->lba;
795 10   33     17 $attrs->{path_be} //= $attrs->{path_be}->lba;
796 10   33     63 $attrs->{root_dirent} //= $self->_encode_dirent("\0", $attrs->{root_file});
797 10   33     65 $attrs->{creation_ts} //= _pack_iso_volume_datetime($attrs->{btime} // $self->{default_time});
      33        
798 10   33     45 $attrs->{modification_ts} //= _pack_iso_volume_datetime($attrs->{mtime} // $self->{default_time});
      33        
799             #$attrs->{effective_ts} //= _pack_iso_volume_datetime($self->{default_time});
800             # Find the filenames of the metadata files
801 10   33     38 $attrs->{vol_set_id} //= $self->volume_set;
802 10   33     327 $attrs->{pub_id} //= $maybe_file_ref->($self->publisher);
803 10   33     162 $attrs->{prep_id} //= $maybe_file_ref->($self->preparer) // "PERL SYS::EXPORT::ISO9660 $VERSION";
      33        
804 10   33     153 $attrs->{app_id} //= $maybe_file_ref->($self->application);
805 10   33     158 $attrs->{copy_id} //= $self->_get_metadata_filename($self->copyright_file, $is_joliet);
806 10   33     152 $attrs->{abs_id} //= $self->_get_metadata_filename($self->abstract_file, $is_joliet);
807 10   33     164 $attrs->{bib_id} //= $self->_get_metadata_filename($self->bibliographic_file, $is_joliet);
808              
809 10 100       29 return _pack_fields(\@vol_desc_fields, $attrs, $is_joliet? 'UTF16-BE' : undef);
810             }
811              
812 30     30   107 sub _get_metadata_filename($self, $spec, $is_joliet) {
  30         29  
  30         27  
  30         26  
  30         25  
813 30         30 my $ent;
814             # Find the root directory entry which refers to this file
815 30 100 66     72 if (blessed($spec) && $spec->can('device_offset')) { # file object
816 2 50       5 ($ent)= grep $_->{file} == $spec, $self->root->entries->@*
817             or croak "Can't find ".$spec->name." in root directory";
818             } else {
819 28 50       93 return '' unless length $spec;
820 0   0     0 $ent= $self->root->entry($spec) // croak "Can't find $spec in root directory";
821             }
822             # Return the name as it exists in this directory entry
823 2 100       8 !$is_joliet? $ent->{shortname}.';1' : $ent->{name};
824             }
825              
826             our @boot_catalog_descriptor= (
827             [ type_code => 0x000, 1, 'C', 0 ],
828             [ std_id => 0x001, 5, 'A', 'CD001' ],
829             [ version => 0x006, 1, 'C', 1 ],
830             [ system_id => 0x007, 32, 'A', 'EL TORRITO SPECIFICATION' ],
831             [ boot_id => 0x027, 32, 'A', '' ],
832             [ extent_lba => 0x047, 4, 'V' ]
833             );
834              
835             # Boot Catalog Validation Entry (first 32 bytes)
836             #our @boot_catalog_validation_entry = (
837             # [ header_id => 0x00, 1, 'C', 0x01 ], # must be 0x01
838             # [ platform => 0x01, 1, 'C', 0x00 ], # 0=x86, 1=PPC, 2=Mac, 0xEF=EFI
839             # #[ reserved1 => 0x02, 2, 'v', 0 ],
840             # [ id_string => 0x04, 24, 'A', 'EL TORITO SPECIFICATION' ],
841             # [ checksum => 0x1C, 2, 'v', 0 ], # to be filled in later
842             # [ key => 0x1E, 2, 'v', 0xAA55 ],
843             #);
844              
845             # Section Header (32 bytes)
846             our @boot_catalog_section_header = (
847             [ header_id => 0x00, 1, 'C', 0x91 ], # 0x91=section, 0x90=final section
848             [ platform => 0x01, 1, 'C', 0x00 ],
849             [ entries_cnt => 0x02, 2, 'v' ], # number of entries following
850             [ id_string => 0x04, 28, 'A', '' ],
851             );
852              
853             # Boot Entry (32 bytes)
854             our @boot_catalog_entry = (
855             [ bootable => 0x00, 1, 'C', 0x88 ], # 0x88 bootable, 0x00 not
856             [ media_type => 0x01, 1, 'C', 0x00 ], # 0=no-emul, 1=1.2M, 2=1.44M, 3=2.88M, 4=HDD
857             [ load_segment => 0x02, 2, 'v', 0x0000 ], # 0x0000=no-emul, 0x07C0=floppy
858             [ system_type => 0x04, 1, 'C', 0x00 ], # MBR partition type byte, 0xEF for EFI ESP
859             #[ reserved1 => 0x05, 1, 'C', 0 ],
860             [ sector_count => 0x06, 2, 'v', 0 ], # size in 512-byte sectors
861             [ sector_start => 0x08, 4, 'V', 0 ], # LBA of boot image
862             [ reserved2 => 0x0C, 20, 'a', '' ],
863             );
864              
865 6     6   98 sub _calc_boot_catalog_size($self) {
  6         7  
  6         6  
866 6 50       11 my $boot_catalog= $self->boot_catalog
867             or return 0;
868             my $extent= $boot_catalog->{extent}
869 6   66     26 //= Sys::Export::Extent->new(name => 'El Torrito Boot Catalog', block_size => 2048);
870 6         8 my $size= 32;
871 6         14 for my $s ($boot_catalog->{sections}->@*) {
872 8         18 $size += 32 + 32 * $s->{entries}->@*;
873             }
874 6         14 $extent->size($size);
875 6         7 $size;
876             }
877              
878             # The boot catalog is one (or more, unlikely) sector containing a list of 32-byte entries
879             # divided into "sections".
880 3     3   5 sub _pack_boot_catalog($self) {
  3         4  
  3         3  
881 3         7 my $boot_catalog= $self->boot_catalog;
882 3         6 my $sections= $boot_catalog->{sections};
883             my $catalog= pack 'C C @4 A24',
884             1, # header_id = 1
885             $sections->[0]{platform}, # copy section0's platform
886 3         12 'EL TORITO SPECIFICATION';
887            
888 3         37 $catalog .= pack 'v v',
889             sum(unpack 'v*', $catalog), # Checksum
890             0xAA55; # key bytes
891              
892 3         18 for my $s (@$sections) {
893             $catalog .= _pack_fields(\@boot_catalog_section_header, {
894             %$s,
895             header_id => ($s == $sections->[-1] ? 0x90 : 0x91), # indicates last section
896 4 100       32 entries_cnt => scalar($s->{entries}->@*), # num entries in section
897             });
898 4         16 for ($s->{entries}->@*) {
899             $catalog .= _pack_fields(\@boot_catalog_entry, {
900             %$_,
901             # The lba and sector count are measured in 512-byte sectors like BIOS,
902             # rather than 2048 sectors like the rest of ISO9660
903             sector_start => $_->{extent}->lba,
904 4         18 sector_count => ceil($_->{extent}->size / 512),
905             });
906             }
907             }
908             die "BUG: encoded size doesn't match file->size"
909 3 50       9 if length($catalog) != $boot_catalog->{extent}->size;
910 3         9 $boot_catalog->{extent}->data(\$catalog);
911             }
912              
913             # The boot catalog descriptor is one of the descriptors at the start of the image which
914             # tells the extent where the boot catalog can be found.
915 3     3   4 sub _pack_boot_catalog_descriptor($self) {
  3         5  
  3         5  
916 3         8 my $boot_catalog= $self->boot_catalog;
917 3         15 my %attrs= ( %$boot_catalog, extent_lba => $boot_catalog->{extent}->lba );
918 3         12 return _pack_fields(\@boot_catalog_descriptor, \%attrs);
919             }
920              
921             # Avoiding dependency on namespace::clean
922             delete @{Sys::Export::ISO9660::}{qw(
923             carp croak confess blessed dualvar refaddr weaken min max sum time ceil encode decode
924             S_IFDIR S_ISDIR S_ISREG SEEK_SET SEEK_END
925             write_file_extent expand_stat_shorthand isa_handle isa_array isa_hash
926             )};
927             1;
928              
929             __END__