| 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__ |