File Coverage

blib/lib/Sys/Export/VFAT.pm
Criterion Covered Total %
statement 474 536 88.4
branch 137 238 57.5
condition 99 190 52.1
subroutine 42 47 89.3
pod 19 19 100.0
total 771 1030 74.8


line stmt bran cond sub pod time code
1             package Sys::Export::VFAT;
2             # ABSTRACT: Write minimal FAT12/16/32 filesystems with control over stored file extents
3             our $VERSION = '0.006'; # VERSION
4              
5 3     3   194598 use v5.26;
  3         8  
6 3     3   10 use warnings;
  3         4  
  3         115  
7 3     3   10 use experimental qw( signatures );
  3         4  
  3         13  
8 3     3   308 use Fcntl qw( S_IFDIR S_ISDIR S_ISREG );
  3         7  
  3         164  
9 3     3   20 use Scalar::Util qw( blessed dualvar refaddr weaken );
  3         9  
  3         142  
10 3     3   10 use List::Util qw( min max );
  3         4  
  3         164  
11 3     3   13 use POSIX 'ceil';
  3         3  
  3         19  
12 3     3   568 use Sys::Export::LogAny '$log';
  3         7  
  3         14  
13 3     3   962 use Encode qw( encode decode );
  3         11899  
  3         152  
14 3     3   13 use Carp;
  3         3  
  3         365  
15             our @CARP_NOT= qw( Sys::Export Sys::Export::Unix );
16             use constant {
17 3         312 ATTR_READONLY => dualvar(0x01, 'ATTR_READONLY'),
18             ATTR_HIDDEN => dualvar(0x02, 'ATTR_HIDDEN'),
19             ATTR_SYSTEM => dualvar(0x04, 'ATTR_SYSTEM'),
20             ATTR_VOLUME_ID => dualvar(0x08, 'ATTR_VOLUME_ID'),
21             ATTR_DIRECTORY => dualvar(0x10, 'ATTR_DIRECTORY'),
22             ATTR_ARCHIVE => dualvar(0x20, 'ATTR_ARCHIVE'),
23             ATTR_LONG_NAME => dualvar(0x0F, 'ATTR_LONG_NAME'),
24             ATTR_LONG_NAME_MASK => 0x3F,
25 3     3   13 };
  3         4  
26 3     3   13 use Exporter 'import';
  3         4  
  3         147  
27             our @EXPORT_OK= qw( FAT12 FAT16 FAT32 ATTR_READONLY ATTR_HIDDEN ATTR_SYSTEM ATTR_ARCHIVE
28             ATTR_VOLUME_ID ATTR_DIRECTORY is_valid_longname is_valid_shortname is_valid_volume_label
29             build_shortname remove_invalid_shortname_chars );
30 3     3   432 use Sys::Export qw( isa_hash isa_array isa_handle isa_int isa_pow2 expand_stat_shorthand write_file_extent );
  3         8  
  3         23  
31 3     3   1516 use Sys::Export::VFAT::Geometry qw( FAT12 FAT16 FAT32 );
  3         7  
  3         21944  
32             require Sys::Export::VFAT::AllocationTable;
33             require Sys::Export::VFAT::File;
34             require Sys::Export::VFAT::Directory;
35              
36              
37 31     31 1 99582 sub new($class, @attrs) {
  31         65  
  31         62  
  31         44  
38             my %attrs= @attrs != 1? @attrs
39 31 50       148 : isa_hash $attrs[0]? %{$attrs[0]}
  0 50       0  
    100          
40             : isa_handle $attrs[0]? ( filehandle => $attrs[0] )
41             : ( filename => $attrs[0] );
42 31         80 my $self= bless {}, $class;
43 31         95 $self->{root}= $self->_new_dir('/', undef, undef);
44             # keep root dir separate from subdirs
45 31         95 delete $self->{_subdirs}{refaddr $self->{root}};
46             # apply other attributes
47 31         115 $self->$_($attrs{$_}) for keys %attrs;
48 31         97 $self;
49             }
50              
51             # Create dir, and store a strong reference in ->{_subdirs}
52 65     65   78 sub _new_dir($self, $name, $parent, $file) {
  65         68  
  65         84  
  65         60  
  65         61  
  65         60  
53 65         243 my $dir= Sys::Export::VFAT::Directory->new(name => $name, parent => $parent, file => $file);
54 65         184 $self->{_subdirs}{refaddr $dir}= $dir;
55 65         123 $dir;
56             }
57              
58              
59 0 0   0 1 0 sub filename { @_ > 1? ($_[0]{filename}= $_[1]) : $_[0]{filename} }
60 93 100   93 1 383 sub filehandle { @_ > 1? ($_[0]{filehandle}= $_[1]) : $_[0]{filehandle} }
61              
62              
63 21262     21262 1 20438 sub root($self) { $self->{root} }
  21262         20876  
  21262         18976  
  21262         38660  
64 212     212 1 1447 sub geometry($self) { $self->{geometry} }
  212         224  
  212         202  
  212         422  
65 33     33 1 35 sub allocation_table($self) { $self->{allocation_table} }
  33         34  
  33         42  
  33         56  
66              
67 310     310 1 405 sub volume_offset($self, @val) {
  310         361  
  310         311  
  310         322  
68 310 100       487 if ($self->{geometry}) {
69 31 50       54 croak "Geometry already decided" if @val;
70 31         84 return $self->{geometry}->volume_offset;
71             }
72 279 100       472 if (@val) {
73 20 50       48 croak "volume_offset must be a multiple of 512" if $val[0] & 511;
74 20         50 return $self->{volume_offset}= $val[0];
75             }
76 259   100     886 $self->{volume_offset} // 0
77             }
78              
79              
80 256     256 1 263 sub min_bits($self, @val) {
  256         284  
  256         240  
  256         212  
81 256 50       302 if (@val) {
82 0 0       0 croak "Geometry already decided" if $self->{geometry};
83 0         0 $self->{min_bits}= $val[0];
84             }
85             $self->{min_bits}
86 256         826 }
87              
88 34     34 1 43 sub bytes_per_sector($self, @val) {
  34         38  
  34         44  
  34         52  
89 34 50       72 if ($self->{geometry}) {
90 0 0       0 croak "Geometry already decided" if @val;
91 0         0 return $self->{geometry}->bytes_per_sector;
92             }
93 34 100       59 if (@val) {
94 3 50       8 croak "bytes_per_sector must be a power of 2" unless isa_pow2 $val[0];
95 3         10 return $self->{bytes_per_sector}= $val[0];
96             }
97 31   100     124 $self->{bytes_per_sector} // 512
98             }
99              
100 37     37 1 44 sub sectors_per_cluster($self, @val) {
  37         45  
  37         47  
  37         33  
101 37 50       63 if ($self->{geometry}) {
102 0 0       0 croak "Geometry already decided" if @val;
103 0         0 return $self->{geometry}->sectors_per_cluster;
104             }
105 37 100       51 if (@val) {
106 3 50 33     13 croak "sectors_per_cluster must be a power of 2, and 128 or less" unless isa_pow2 $val[0] && $val[0] <= 128;
107 3         12 return $self->{sectors_per_cluster}= $val[0];
108             }
109             $self->{sectors_per_cluster}
110 34         101 }
111              
112 256     256 1 294 sub fat_count($self, @val) {
  256         294  
  256         229  
  256         242  
113 256 50       385 if ($self->{geometry}) {
114 0 0       0 croak "Geometry already decided" if @val;
115 0         0 return $self->{geometry}->fat_count;
116             }
117 256 50       330 if (@val) {
118 0 0 0     0 croak "fat_count must be positive" unless isa_int $val[0] && $val[0] > 0;
119 0         0 return $self->{fat_count}= $val[0];
120             }
121             $self->{fat_count}
122 256         519 }
123              
124 0     0 1 0 sub free_space($self, @val) {
  0         0  
  0         0  
  0         0  
125 0 0       0 if (@val) {
126 0 0       0 croak "Geometry already decided" if $self->{geometry};
127 0         0 return $self->{free_space}= $val[0];
128             }
129 0   0     0 $self->{free_space} // 0
130             }
131              
132 62     62 1 75 sub volume_label($self, @val) {
  62         68  
  62         68  
  62         67  
133 62 50       90 if (@val) {
134 0 0       0 croak "Invalid volume label '$val[0]'" unless is_valid_volume_label($val[0]);
135 0         0 return $self->{volume_label}= $val[0];
136             }
137             $self->{volume_label}
138 62         247 }
139              
140             # The smallest conceivable address where the data region could start
141             sub _minimum_offset_to_data {
142 1     1   9 state $minimum_offset_to_data= Sys::Export::VFAT::Geometry->new(
143             bytes_per_sector => 512,
144             sectors_per_cluster => 1,
145             fat_count => 1,
146             root_dirent_count => 1,
147             cluster_count => 1
148             )->data_start_offset;
149             }
150              
151              
152 10517     10517 1 31954 sub add($self, $spec) {
  10517         10606  
  10517         9968  
  10517         9095  
153 10517 100       18177 $spec= { expand_stat_shorthand($spec) }
154             if isa_array $spec;
155              
156             defined $spec->{uname} or defined $spec->{name}
157 10517 50 33     28799 or croak "Require 'uname' or 'name'";
158 10517 50       14569 defined $spec->{mode} or croak "Require 'mode'";
159              
160             # If user supplied uname, use that as a more official source of Unicode
161 10517   33     42261 my $path= $spec->{uname} // decode('UTF-8', $spec->{name}, Encode::FB_CROAK | Encode::LEAVE_SRC);
162 10517         255506 $path =~ s,^/,,; # remove leading slash, if any
163              
164 10517         71041 my @path= grep length, split '/', $path;
165 10517         18900 my $leaf= pop @path;
166              
167             # Walk through the tree based on the case-folded path
168 10517         17998 my $parent= $self->root;
169 10517         13671 for (@path) {
170 98362         119968 my $ent= $parent->entry($_);
171 98362 100       102943 if ($ent) {
172             croak $ent->name." is not a directory, while attempting to add '$path'"
173 98329 50       125040 unless $ent->{dir};
174             } else { # Auto-create directory. Autovivication is indicated by ->{file} = undef
175 33         45 $ent= $parent->add($_, undef);
176 33 100       44 my $name= ($parent == $self->root? '' : $parent->name)."/$_";
177 33         48 weaken($ent->{dir}= $self->_new_dir($name, $parent, undef));
178             }
179 98362         100459 $parent= $ent->{dir};
180             }
181              
182             # did user supply FAT attribute bitmask?
183 10517   33     16615 my $flags= $spec->{FAT_flags} // do {
184             # readonly determined by user -write bit of 'mode'
185 10517 50 33     36491 (!($spec->{mode} & 0400)? ATTR_READONLY : 0)
    50          
186             # hidden determined by leading '.' in filename
187             | (defined $leaf && $leaf =~ /^\./? ATTR_HIDDEN : 0)
188             };
189 10517         17510 my $file;
190 10517 100       22431 if (S_ISREG($spec->{mode})) {
    50          
191             my ($size, $offset, $align, $data_ref)
192 10516         10976 = @{$spec}{qw( size device_offset device_align data )};
  10516         17865  
193 10516 100 66     21317 $data_ref= do { my $x= $data_ref; \$x }
  1573         1637  
  1573         1902  
194             if defined $data_ref && !ref $data_ref;
195 10516 50       16917 if ($size) {
    50          
196             # ensure data matches
197 0 0       0 croak "File $path has size=$size but lacks 'data' attribute"
198             unless defined $data_ref;
199 0 0       0 croak "File $path ->{data} length disagrees with ->{size}"
200             unless length($$data_ref) == $size;
201             } elsif (defined $data_ref) {
202 10516   33     19265 $size //= length($$data_ref);
203             }
204             # must be a power of 2
205 10516 50 66     14826 croak "Invalid device_align $align for '$path', must be a power of 2"
206             if defined $align && !isa_pow2 $align;
207             # Sanity check device_offset before we get too far along
208 10516 100       12529 if (defined $offset) {
209 1   50     10 $align //= 512;
210             # must fall in the data area
211 1 50 33     2 $offset > $self->volume_offset + _minimum_offset_to_data
212             # must be a multiple of at least 512 (probably more)
213             && !($offset & ($align-1))
214             or croak "Invalid device_offset '$offset' for file '$path'";
215             }
216             $file= Sys::Export::VFAT::File->new(
217             name => "/$path", size => $size, flags => $flags,
218             align => $align, device_offset => $offset, data => $data_ref,
219 10516         37326 $spec->%{qw( atime btime mtime )},
220             );
221             } elsif (S_ISDIR($spec->{mode})) {
222 1         2 $flags |= ATTR_DIRECTORY;
223             # If adding this directory overtop a previous auto-vivified directory, the ->{file}
224             # will be empty and we can just update it.
225 1         3 my $cur= $parent->entry($leaf);
226             croak "Attempt to add duplicate directory $leaf"
227 1 0 33     2 if $cur && $cur->{file};
228             $file= Sys::Export::VFAT::File->new(
229 1         9 name => "/$path", size => 0, flags => $flags, $spec->%{qw( atime btime mtime )},
230             );
231 1 50       2 if ($cur) {
232 0         0 $cur->{file}= $file;
233 0         0 $cur->{dir}{file}= $file;
234 0         0 $log->debugf("updated attributes of %s", $path);
235 0         0 return $cur;
236             }
237             # otherwise, add this file to a directory entry
238             }
239             else {
240             # TODO: add conditional symlink support via hardlinks
241 0         0 croak "Can only export files or directories into VFAT"
242             }
243              
244             # this also checks for name collisions on shortname
245 10517         28117 my $ent= $parent->add($leaf, $file, shortname => $spec->{FAT_shortname});
246             # If the dirent is a directory, also add a directory object to the dirent
247 10517 100       20610 if ($file->is_dir) {
248             # the directory object also gets a reference to its file object.
249 1         4 weaken($ent->{dir}= $self->_new_dir("/$path", $parent, $file));
250             }
251              
252             $log->debugf("added %s longname=%s shortname=%s %s",
253             $path, $ent->{name}, $ent->{shortname}//'', join(' ',
254             !$ent->{file}? ('size=0 (empty file)')
255             : $ent->{file}->is_dir? ('DIR')
256             : (
257             (defined $ent->{file}->size? sprintf("size=0x%X", $ent->{file}->size) : 'size='),
258             (defined $ent->{file}->align? sprintf("device_align=0x%X", $ent->{file}->align) : ()),
259 10517 0 0     21592 (defined $ent->{file}->device_offset? sprintf("device_offset=0x%X", $ent->{file}->device_offset) : ())
    0          
    0          
    0          
    0          
    50          
260             )
261             ))
262             if $log->is_debug;
263              
264 10517         82529 $ent->{file};
265             }
266              
267              
268 31     31 1 110 sub finish($self) {
  31         36  
  31         48  
269 31         58 my $root= $self->root;
270 31         97 $log->debug('begin VFAT::finish');
271             # Find out the size of every directory, and build ->{_allocs}, ->{_dir_allocs} and ->{_special_allocs}
272 31         381 $self->_calc_dir_size($_) for $root, values $self->{_subdirs}->%*;
273             # calculate what geometry gives us the best size, when rounding each file to that cluster
274             # size vs. the size of the FAT it generates
275             my ($geom, $alloc)= $self->_optimize_geometry
276             or croak join("\n", "No geometry options can meet your device_offset / device_align requests:",
277             map "$_: $self->{_optimize_geometry_fail_reason}{$_}",
278 0         0 sort { $a <=> $b } keys $self->{_optimize_geometry_fail_reason}->%*
279 31 50       85 );
280 31         73 $self->{geometry}= $geom;
281 31         74 $self->{allocation_table}= $alloc;
282 31         66 $self->_commit_allocation; # copy cluster IDs into each of the File objects
283             # Pack directories now that all file cluster ids are known
284 31         138 $self->_pack_directory($_) for $root, values $self->{_subdirs}->%*;
285              
286 31         70 my $fh= $self->filehandle;
287 31 50       140 if (!$fh) {
288 0 0       0 defined $self->filename or croak "Must set filename or filehandle attributes";
289 0 0       0 open $fh, '+>', $self->filename
290             or croak "open: $!";
291             }
292             # check size
293 31 50       571 if (-s $fh < $geom->volume_offset + $geom->total_size) {
294 31         53 $log->debugf('resize output file to %s', $geom->volume_offset + $geom->total_size);
295 31 50       217 truncate($fh, $geom->volume_offset + $geom->total_size)
296             or croak "truncate: $!";
297             }
298 31         131 $self->_write_filesystem($fh, $geom, $alloc);
299 31 50       1050 unless ($self->filehandle) {
300 0 0       0 $fh->close or croak "close: $!";
301             }
302 31         183 $log->debug('end VFAT::finish');
303 31         329 1;
304             }
305              
306 0     0   0 sub _log_hexdump($buf) {
  0         0  
  0         0  
307             $log->tracef('%04X'.(" %02x"x16), $_, unpack 'C*', substr($buf, $_*16, 16))
308 0         0 for 0..ceil(length($buf) / 16);
309             }
310              
311 31     31   39 sub _write_filesystem($self, $fh, $geom, $alloc) {
  31         42  
  31         33  
  31         30  
  31         29  
  31         25  
312 31 50 50     89 ($alloc->max_cluster_id//-1) == ($geom->max_cluster_id//-1)
      50        
313             or croak "Max element of 'fat_entries' should be ".$geom->max_cluster_id.", but was ".$alloc->max_cluster_id;
314             # Pack the boot sector and other reserved sectors
315 31         71 my $buf= $self->_pack_reserved_sectors;
316 31         82 my $ofs= $self->volume_offset;
317 31         86 write_file_extent($fh, $ofs, $geom->reserved_size, \$buf, 0, 'reserved sectors');
318 31         76 $ofs += $geom->reserved_size;
319             # Pack the allocation tables
320 31         73 $buf= $self->_pack_allocation_table($alloc);
321             # store a copy of this into each of the regions occupied by fats
322 31         82 for (my $i= 0; $i < $geom->fat_count; $i++) {
323 62         108 write_file_extent($fh, $ofs, $geom->fat_size, \$buf, 0, "fat table $i");
324 62         139 $ofs += $geom->fat_size;
325             }
326             # For FAT12/FAT16, write the root directory entries
327 31 100       46 if ($geom->bits < FAT32) {
328 30         58 my $rootf= $self->root->file;
329             die "BUG: mis-sized FAT16 root directory"
330             if !$rootf->size || ($rootf->size & 31)
331 30 50 33     72 || length ${$rootf->data} != $rootf->size
  30   33     54  
      33        
332             || $rootf->size > $geom->root_dir_size;
333 30         51 write_file_extent($fh, $ofs, $geom->root_dir_size, $rootf->data, 0, 'root dir');
334             }
335             # The files and dirs have all been assigned clusters by _optimize_geometry
336 31         83 for my $cl (sort { $a <=> $b } keys $alloc->chains->%*) {
  114111         93275  
337 10551         19440 my ($invlist, $file)= $alloc->chains->{$cl}->@{'invlist','file'};
338 10551         17579 my $data= $file->data;
339             # Given an inversion list describing the allocated clusters for this file,
340             # write the relevant chunks of the file to those cluster data areas.
341 10551 50       17986 $log->debugf("writing '%s' at cluster %s", $file->name, _render_invlist($invlist))
342             if $log->is_debug;
343 10551         49664 my $data_ofs= 0;
344 10551         14938 for (my $i= 0; $i < @$invlist; $i += 2) {
345 10551         10185 my ($cl_start, $cl_lim)= @{$invlist}[$i, $i+1];
  10551         14339  
346 10551         17973 my $size= ($cl_lim-$cl_start) * $geom->bytes_per_cluster;
347 10551         15342 write_file_extent($fh, $geom->get_cluster_device_offset($cl_start), $size, $data, $data_ofs);
348 10551         22870 $data_ofs += $size;
349             }
350             }
351             }
352 0     0   0 sub _render_invlist($il) {
  0         0  
  0         0  
353 0 0       0 join ',',
354             map +($il->[$_*2] == $il->[$_*2+1]-1? $il->[$_*2] : $il->[$_*2] . '-' . $il->[$_*2+1]),
355             0 .. int($#$il/2)
356             }
357              
358              
359 10556     10556 1 13332 sub is_valid_longname($name) {
  10556         11274  
  10556         9994  
360             # characters permitted for LFN are all letters numbers and $%'-_@~`!(){}^#&+,;=[].
361             # and space and all codepoints above 0x7F.
362             # they may not begin with space, and cannot exceed 255 chars.
363 10556   33     45932 !!($name !~ /^(\.+\.?)\z/ # dot and dotdot are reserved
364             && $name =~ /^
365             [^\x00-\x20\x22\x2A\x2F\x3A\x3C\x3E\x3F\x5C\x7C\x7F]
366             [^\x00-\x1F\x22\x2A\x2F\x3A\x3C\x3E\x3F\x5C\x7C]{0,254}
367             \z/x);
368             }
369              
370 10602     10602 1 174919 sub is_valid_shortname($name) {
  10602         10355  
  10602         10033  
371 10602   100     65312 !!($name eq uc $name
372             && $name =~ /^
373             [\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D\x7E\x80-\xFF]
374             [\x20\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D\x7E\x80-\xFF]{0,7}
375             (?: \.
376             ( [\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D-\xFF]
377             [\x20\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D-\xFF]{0,2}
378             )?
379             )?
380             \z/x);
381             }
382              
383 0     0 1 0 sub is_valid_volume_label($name) {
  0         0  
  0         0  
384             # same as shortname but no '.' and space is allowed
385 0         0 !!($name =~ /^
386             [\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D-\xFF]
387             [\x20\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D-\xFF]{0,10}
388             \z/x);
389             }
390              
391 72     72 1 71 sub remove_invalid_shortname_chars($name, $replacement='_') {
  72         69  
  72         72  
  72         63  
392 72         83 $name =~ tr/a-z/A-Z/; # perform 'uc' but only for the ASCII range
393 72         137 $name =~ s/[^\x20\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D\x7E\x80-\xFF]+/$replacement/gr;
394             }
395              
396 31     31   35 sub _optimize_geometry($self) {
  31         39  
  31         32  
397             # calculate what geometry gives us the best size, when rounding each file to that cluster
398             # size vs. the size of the FAT it generates, and also meting the needs of alignment requests
399 31         78 my $root= $self->root;
400 31         37 my (@offsets, @aligned, @others);
401 31         97 my %seen= ( refaddr($root) => 1 );
402 31         103 for my $dir ($root, values $self->{_subdirs}->%*) {
403 65         117 for my $ent ($dir->entries->@*) {
404             # entry may have a directory ref and not a direct file ref
405 10550   33     15541 my $file= $ent->{file} //= $ent->{dir} && $ent->{dir}->file;
      66        
406 10550 50 33     31879 next unless $file && !$seen{refaddr $file}++;
407 10550 100       8955 push @{$file->device_offset? \@offsets : $file->align? \@aligned : \@others}, $file;
  10550 100       11712  
408             }
409             }
410 31         148 $log->debugf("_optimize_geometry offsets=%d aligned=%d others=%d",
411             scalar @offsets, scalar @aligned, scalar @others);
412             # provide stable results
413 31         319 @offsets= sort { $a->device_offset <=> $b->device_offset } @offsets;
  0         0  
414 31         59 @aligned= sort { fc $a->name cmp fc $b->name } @aligned;
  18         38  
415 31         218 @others= sort { fc $a->name cmp fc $b->name } @others;
  22622         26829  
416 31         104 my $min_ofs= min(map $_->device_offset, @offsets);
417 31         63 my $max_ofs= max(map $_->device_offset + $_->size, @offsets);
418 31         70 my $max_align= max(0, map $_->align, @aligned);
419 31         64 my $root_dirent_used= $root->file->size / 32;
420 31 50 33     83 isa_int $root_dirent_used && $root_dirent_used >= 1
421             or die "BUG: root must always have one entry";
422 31         83 my $bytes_per_sector= $self->bytes_per_sector;
423 31         48 my %fail_reason;
424             my $best;
425             # If the user defined sectors_per_cluster, we only have one option.
426             # Otherwise iterate through all of them to find the best.
427 31 100       55 my @spc= defined $self->sectors_per_cluster? ( $self->sectors_per_cluster )
428             : (1,2,4,8,16,32,64,128);
429 31         55 cluster_size: for my $sectors_per_cluster (@spc) {
430 227         293 my $cluster_size= $sectors_per_cluster * $bytes_per_sector;
431 227 50       411 isa_pow2 $cluster_size or die "BUG: cluster_size not a power of 2";
432             # Avoid triggering warning about incompatible cluster sizes if a good cluster size was
433             # already found.
434 227 100 100     580 last if $best && $cluster_size > 32*1024;
435             # Count total sectors used by ->{size} of files and dirs.
436             # Don't add root dir until we know it will be FAT32
437 199         195 my $clusters= 0;
438 199         373 for (@offsets, @aligned, @others) {
439 20000         24495 $clusters += ceil($_->size / $cluster_size);
440             }
441 199         625 $log->tracef("with sectors_per_cluster=%d, would require at least %d clusters",
442             $sectors_per_cluster, $clusters);
443 199   100     1794 $clusters ||= 1;
444 199         221 my ($reserved, $root_clusters_added);
445             # If file alignment is a larger power of 2 than cluster_size, then as long as data_start
446             # is aligned to cluster_size there will be a cluster that can satisfy the alignment.
447             # If file alignment is a smaller power of 2 than cluster_size, then as long as
448             # data_start is aligned to the file alignment, every cluster can satisfy the alignment.
449 199         364 my $align= min($cluster_size, $max_align);
450 199 100       314 if ($align) {
    100          
451             # But wait, does every device_offset meet this alignment? If not, give up.
452 133         196 for (@offsets) {
453 0 0       0 if ($_->device_offset & ($align-1)) {
454 0         0 $fail_reason{$sectors_per_cluster}= "device_offset ".$_->device_offset
455             ." of ".$_->name." conflicts with your alignment request of $align";
456 0         0 next cluster_size;
457             }
458             }
459             }
460             elsif (@offsets) {
461             # If not aligning clusters to pow2, might need to align to device_offset.
462             # First, every device_offset must have the same remainder modulo cluster_size.
463 7         10 my ($remainder, $prev);
464 7         20 for (@offsets) {
465 7         15 my $r= $_->device_offset & ($cluster_size-1);
466 7 50       13 if (!defined $remainder) {
    0          
467 7         7 $remainder= $r;
468 7         10 $prev= $_;
469             } elsif ($remainder != $r) {
470 0         0 $fail_reason{$sectors_per_cluster}= "file $_->{name} device_offset "
471             .$_->device_offset." modulo cluster_size $cluster_size conflicts with"
472             ." file ".$prev->name." device_offset ".$prev->device_offset;
473 0         0 next cluster_size;
474             }
475             }
476 7 50       12 if ($remainder) {
477 0         0 $align= [ $cluster_size, $remainder ];
478             } else {
479 7         8 $align= $cluster_size;
480             }
481             }
482             again_with_more_clusters: {
483             # If this number of clusters pushes us into FAT32, also need to add the root directory
484             # clusters to the count.
485 256 100 66     246 if (!$root_clusters_added
  256         641  
486             && $clusters > Sys::Export::VFAT::Geometry::FAT16_IDEAL_MAX_CLUSTERS()
487             ) {
488 1         8 $root_clusters_added= ceil($root->file->size / $cluster_size);
489 1         3 $clusters += $root_clusters_added;
490 1         4 $log->tracef("reached FAT32 threshold, adding %s clusters for root dir", $root_clusters_added);
491             }
492 256         506 my $geom= Sys::Export::VFAT::Geometry->new(
493             volume_offset => $self->volume_offset,
494             (align_clusters => $align)x!!$align,
495             bytes_per_sector => $bytes_per_sector,
496             sectors_per_cluster => $sectors_per_cluster,
497             fat_count => $self->fat_count,
498             cluster_count => $clusters,
499             used_root_dirent_count => $root_dirent_used,
500             min_bits => $self->min_bits,
501             );
502 256         557 $log->debugf("testing clusters=%d size=0x%X data_region=0x%X-%X min_ofs=0x%X max_ofs=0x%X",
503             $clusters, $cluster_size, $geom->data_start_device_offset, $geom->data_limit_device_offset,
504             $min_ofs, $max_ofs);
505 256 100 100     2211 if (@offsets || @aligned) {
506             # tables are too large? Try again with larger clusters.
507 197 50 66     419 if (defined $min_ofs && $min_ofs < $geom->data_start_device_offset) {
508 0         0 $fail_reason{$sectors_per_cluster}= "FAT tables too large for requested device_offset $min_ofs";
509 0         0 next cluster_size;
510             }
511             # Not enough clusters? Try again with more.
512 197 100 100     412 if (defined $max_ofs && $max_ofs > $geom->data_limit_device_offset) {
513             # This might overshoot a bit since the tables also grow and push forward the
514             # whole data area.
515 7         13 $clusters= ceil(($max_ofs - $geom->data_start_device_offset) / $cluster_size);
516 7         168 goto again_with_more_clusters;
517             }
518             }
519             # Now verify we have enough clusters by actually alocating them
520 249         720 my $alloc= Sys::Export::VFAT::AllocationTable->new;
521 249         294 my %assignment;
522 249 50       309 unless (eval {
523             $self->_alloc_file($geom, $alloc, $_)
524 249 100       530 for @offsets, @aligned, @others, ($geom->bits == FAT32? ($root->file) : ());
525 249         502 1
526             }) {
527 0         0 chomp($fail_reason{$sectors_per_cluster}= "$@");
528 0         0 next cluster_size;
529             }
530 249 100       456 if ($alloc->max_used_cluster_id > $geom->max_cluster_id) {
531 50         76 $clusters= $alloc->max_used_cluster_id-1;
532 50         930 goto again_with_more_clusters;
533             }
534             # Allocation worked, so clamp the allocator to this nmber of sectors
535 199         296 $alloc->max_cluster_id($geom->max_cluster_id);
536             # Is this the smallest option so far?
537 199 100 100     543 if (!$best || $best->{geom}->total_sector_count > $geom->total_sector_count) {
538 32         158 $best= { geom => $geom, alloc => $alloc, cluster_assignment => \%assignment };
539             }
540             }
541             } continue {
542             $log->tracef("%s", $fail_reason{$sectors_per_cluster})
543 199 50       565 if defined $fail_reason{$sectors_per_cluster};
544             }
545 31 50       50 if (!$best) {
546 0         0 $log->debug("no cluster size works");
547 0         0 $self->{_optimize_geometry_fail_reason}= \%fail_reason;
548 0         0 return;
549             }
550 31         73 $log->debugf("best cluster_size is %d", $best->{geom}->bytes_per_cluster);
551 31         248 return @{$best}{'geom','alloc'};
  31         3930  
552             }
553              
554             # reserve clusters for a file according to the align/offset needs of that file
555 20101     20101   18744 sub _alloc_file($self, $geom, $alloc, $file) {
  20101         17470  
  20101         16619  
  20101         16114  
  20101         17327  
  20101         16042  
556 20101 50       27832 my $sz= $file->size or do { carp "Attempt to allocate zero-length file"; return };
  0         0  
  0         0  
557 20101         25574 my $cl_count= POSIX::ceil($sz / $geom->bytes_per_cluster);
558 20101         18090 my $cl_start;
559 20101 100       24572 if ($file->device_offset) {
    100          
560 7         15 my ($cl, $n)= $geom->get_cluster_extent_of_device_extent($file->device_offset, $sz);
561 7   33     17 $cl_start= $alloc->alloc_range($cl, $cl_count)
562             // croak "Can't allocate $cl_count clusters from offset ".$file->device_offset;
563             } elsif ($file->align) {
564 359         599 my ($mul, $ofs)= $geom->get_cluster_alignment_of_device_alignment($file->align);
565 359   33     718 $cl_start= $alloc->alloc_contiguous($cl_count, $mul, $ofs)
566             // croak "Can't allocate $cl_count clusters aligned to ".$file->align;
567             } else {
568 19735   33     24021 $cl_start= $alloc->alloc($cl_count)
569             // croak "Can't allocate $cl_count clusters";
570             }
571 20101         25216 $alloc->{chains}{$cl_start}{file}= $file;
572 20101         30034 $cl_start;
573             }
574              
575             # store the cluster and device offset into the File objects
576 31     31   31 sub _commit_allocation($self) {
  31         32  
  31         29  
577 31         66 my $alloc= $self->allocation_table;
578 31         62 my $geom= $self->geometry;
579             # Apply file cluster IDs to the File objects
580 31         69 for (values $alloc->chains->%*) {
581 10551         16499 my $file= $_->{file};
582 10551         21299 $file->{cluster}= $_->{invlist}[0];
583             # Only set offset if file is contiguous
584             $file->{device_offset} //= $geom->get_cluster_device_offset($file->{cluster})
585 10551 50 66     8874 if 2 == @{$_->{invlist}};
  10551         27754  
586             }
587             }
588              
589             our @sector0_fields_common= (
590             [ BS_jmpBoot => 0, 3, 'a3', '' ],
591             [ BS_OEMName => 3, 8, 'a8', 'MSWIN4.1' ],
592             [ BPB_BytsPerSec => 11, 2, 'v' ],
593             [ BPB_SecPerClus => 13, 1, 'C' ],
594             [ BPB_RsvdSecCnt => 14, 2, 'v' ],
595             [ BPB_NumFATs => 16, 1, 'C' ],
596             [ BPB_RootEntCnt => 17, 2, 'v' ],
597             [ BPB_TotSec16 => 19, 2, 'v' ],
598             [ BPB_Media => 21, 1, 'C', 0xF8 ],
599             [ BPB_FATSz16 => 22, 2, 'v' ],
600             [ BPB_SecPerTrk => 24, 2, 'v', 0 ],
601             [ BPB_NumHeads => 26, 2, 'v', 0 ],
602             [ BPB_HiddSec => 28, 4, 'V', 0 ],
603             [ BPB_TotSec32 => 32, 4, 'V' ]
604             );
605             our @sector0_fat16_fields= (
606             @sector0_fields_common,
607             [ BS_DrvNum => 36, 1, 'C', 0x80 ],
608             [ BS_Reserved1 => 37, 1, 'C', 0 ],
609             [ BS_BootSig => 38, 1, 'C', 0x29 ],
610             [ BS_VolID => 39, 4, 'V' ],
611             [ BS_VolLab => 43, 11, 'A11', 'NO NAME' ],
612             [ BS_FilSysType => 54, 8, 'A8' ],
613             [ _signature => 510, 2, 'v', 0xAA55 ],
614             );
615             our @sector0_fat32_fields= (
616             @sector0_fields_common,
617             [ BPB_FATSz32 => 36, 4, 'V' ],
618             [ BPB_ExtFlags => 40, 2, 'v', 0 ],
619             [ BPB_FSVer => 42, 2, 'v', 0 ],
620             [ BPB_RootClus => 44, 4, 'V' ],
621             [ BPB_FSInfo => 48, 2, 'v' ],
622             [ BPB_BkBootSec => 50, 2, 'v', 0 ],
623             [ BPB_Reserved => 52, 12, 'a12', '' ],
624             [ BS_DrvNum => 64, 1, 'C', 0x80 ],
625             [ BS_Reserved1 => 65, 1, 'C', 0 ],
626             [ BS_BootSig => 66, 1, 'C', 0x29 ],
627             [ BS_VolID => 67, 4, 'V' ],
628             [ BS_VolLab => 71, 11, 'A11', 'NO NAME' ],
629             [ BS_FilSysType => 82, 8, 'A8' ],
630             [ _signature => 510, 2, 'v', 0xAA55 ],
631             );
632             our @fat32_fsinfo_fields= (
633             [ FSI_LeadSig => 0, 4, 'V', 0x41615252 ],
634             [ FSI_Reserved1 => 4,480, 'a480', '' ],
635             [ FSI_StrucSig => 484, 4, 'V', 0x61417272 ],
636             [ FSI_Free_Count => 488, 4, 'V', 0xFFFFFFFF ],
637             [ FSI_Nxt_Free => 492, 4, 'V', 0xFFFFFFFF ],
638             [ FSI_Reserved2 => 496, 12, 'a12', '' ],
639             [ FSI_TrailSig => 508, 4, 'V', 0xAA550000 ],
640             );
641 34     34   36 sub _append_pack_args($pack, $vals, $ofs, $fields, $attrs) {
  34         39  
  34         32  
  34         41  
  34         30  
  34         71  
  34         32  
642 34         67 for (@$fields) {
643 700         1182 push @$pack, '@'.($ofs+$_->[1]).$_->[3];
644 700   66     1420 push @$vals, $attrs->{$_->[0]} // $_->[4]
      33        
645             // croak "No value supplied for $_->[0], and no default";
646             }
647             }
648              
649 31947     31947   26596 sub _epoch_to_fat_date_time($epoch) {
  31947         27089  
  31947         25296  
650 31947         168710 my @lt = localtime($epoch);
651 31947         38112 my $year = $lt[5] + 1900;
652 31947         29488 my $mon = $lt[4] + 1;
653 31947         26199 my $mday = $lt[3];
654 31947         26114 my $hour = $lt[2];
655 31947         25166 my $min = $lt[1];
656 31947         32092 my $sec = int($lt[0] / 2); # 2-second resolution
657              
658 31947 50       36541 $year = 1980 if $year < 1980;
659 31947         34393 my $fat_date = (($year - 1980) << 9) | ($mon << 5) | $mday;
660 31947         32024 my $fat_time = ($hour << 11) | ($min << 5) | $sec;
661 31947         30352 my $fat_frac = ($epoch * 100) % 200;
662 31947         46657 return ($fat_date, $fat_time, $fat_frac);
663             }
664              
665             # This packs the boot sector and all the "reserved" sectors that appear before the
666             # beginning of the allocation tables.
667 31     31   55 sub _pack_reserved_sectors($self, %attrs) {
  31         28  
  31         42  
  31         24  
668 31         33 my (@pack, @vals);
669 31         62 my $geom= $self->geometry;
670 31         58 $attrs{BPB_BytsPerSec}= $geom->bytes_per_sector;
671 31         52 $attrs{BPB_SecPerClus}= $geom->sectors_per_cluster;
672 31         58 $attrs{BPB_RsvdSecCnt}= $geom->reserved_sector_count;
673 31         57 $attrs{BPB_NumFATs}= $geom->fat_count;
674 31         86 $attrs{BPB_RootEntCnt}= $geom->root_dirent_count;
675 31   33     128 $attrs{BS_VolLab} //= $self->volume_label;
676 31   33     108 $attrs{BS_VolID} //= time & 0xFFFFFFFF;
677 31 100       57 if ($geom->bits < FAT32) {
678 30 50       54 $attrs{BPB_FATSz16}= $geom->fat_sector_count < 0x10000? $geom->fat_sector_count : 0;
679 30 50       62 $attrs{BPB_FATSz32}= $geom->fat_sector_count < 0x10000? 0 : $geom->fat_sector_count;
680 30 50       49 $attrs{BPB_TotSec16}= $geom->total_sector_count < 0x10000? $geom->total_sector_count : 0;
681 30 50       45 $attrs{BPB_TotSec32}= $geom->total_sector_count < 0x10000? 0 : $geom->total_sector_count;
682 30 100       46 $attrs{BS_FilSysType}= $geom->bits == 12? 'FAT12' : 'FAT16';
683 30         102 _append_pack_args(\@pack, \@vals, 0, \@sector0_fat16_fields, \%attrs);
684             } else {
685             # Did the user specify location of fsinfo? If not, default to sector 1
686 1   50     15 $attrs{BPB_FSInfo} //= 1;
687 1   50     6 $attrs{BPB_BkBootSec} //= 2;
688 1         2 $attrs{BPB_FATSz16}= 0;
689 1         3 $attrs{BPB_FATSz32}= $geom->fat_sector_count;
690 1         3 $attrs{BPB_RootClus}= $self->root->file->cluster;
691 1         2 $attrs{BPB_TotSec16}= 0;
692 1         3 $attrs{BPB_TotSec32}= $geom->total_sector_count;
693 1         2 $attrs{BS_FilSysType}= "FAT";
694 1         4 _append_pack_args(\@pack, \@vals, 0, \@sector0_fat32_fields, \%attrs);
695            
696             # FSInfo struct, location is configurable
697 1         2 my $fsi_ofs= $attrs{BPB_FSInfo} * $attrs{BPB_BytsPerSec};
698 1   33     22 $attrs{FSI_Free_Count} //= $self->allocation_table->free_cluster_count;
699 1   33     5 $attrs{FSI_Nxt_Free} //= $self->allocation_table->first_free_cluster;
700 1         45 _append_pack_args(\@pack, \@vals, $fsi_ofs, \@fat32_fsinfo_fields, \%attrs);
701              
702             # Backup copy of boot sector
703 1         3 my $bk_ofs= $attrs{BPB_BkBootSec} * $attrs{BPB_BytsPerSec};
704 1         3 _append_pack_args(\@pack, \@vals, $bk_ofs, \@sector0_fat32_fields, \%attrs);
705 1         4 _append_pack_args(\@pack, \@vals, $bk_ofs+$fsi_ofs, \@fat32_fsinfo_fields, \%attrs);
706             }
707 31         444 pack join(' ', @pack), @vals;
708             }
709              
710             # This packs one allocation table and returns the buffer
711 31     31   32 sub _pack_allocation_table($self, $alloc) {
  31         29  
  31         33  
  31         28  
712 31         84 my $fat= [ $alloc->fat->@* ];
713 31 50 33     86 croak "Allocation table used more clusters than configured in geometry"
714             if $self->geometry->max_cluster_id < ($alloc->max_cluster_id // $alloc->max_used_cluster_id);
715 31         49 my $max= $self->geometry->max_cluster_id;
716 31         115 $#$fat= $max;
717 31         84 $fat->[$_]= 0x0FFFFFFF for 0,1;
718 31   100     23062 $fat->[$_] //= 0 for 2..$max; # prevent warnings in pack
719 31 100       67 if ($self->geometry->bits == 32) {
    100          
720 1         2533 return pack 'V*', @$fat;
721             } elsif ($self->geometry->bits == 16) {
722 1         153 return pack 'v*', @$fat;
723             } else {
724             # 12 bits per entry, pack in groups of 3 bytes, little-endian
725 29         43 my $buf= "\xFF\xFF\xFF";
726 29         67 for (my $i= 2; $i+1 <= $max; $i+= 2) {
727 1957         2119 my $v= ($fat->[$i] & 0xFFF) | ( ($fat->[$i+1] & 0xFFF) << 12 );
728 1957         2808 $buf .= pack 'vC', $v, ($v >> 16);
729             }
730 29 100       66 $buf .= pack 'v', $fat->[$max] & 0xFFF unless $max & 1;
731 29         147 return $buf;
732             }
733             }
734              
735             # This calculates the encoded size of one directory
736 65     65   65 sub _calc_dir_size($self, $dir) {
  65         79  
  65         82  
  65         59  
737             # If an autovivified directoy lacks a ->{file}, create it.
738 65   66     236 $dir->{file} //= Sys::Export::VFAT::File->new(name => $dir->name, flags => ATTR_DIRECTORY);
739 65         105 my $ents= $dir->entries;
740             # Need the 8.3 name in order to know whether it matches the long name
741 65         157 $dir->build_shortnames;
742             # root dir has a volume label ent, and all other dirs have '.' and '..'
743 65 100       122 my $n= @$ents + ($dir->is_root? 1 : 2);
744 65         102 for (@$ents) {
745             # Add LFN entries
746 10550 100       14014 if ($_->{name} ne $_->{shortname}) {
747 36         116 my $utf16= encode('UTF-16LE', $_->{name}, Encode::FB_CROAK|Encode::LEAVE_SRC);
748 36         3470 $n += ceil(length($utf16) / 26);
749             }
750             }
751             $log->debugf("dir %s has %d real entries, %d LFN entries, size=%d ents=%s",
752             $dir->name, scalar(@$ents), $n-scalar @$ents, $n*32,
753 65 0       133 [ map +( $_->{name} eq $_->{shortname}? $_->{name} : [ @{$_}{'name','shortname'} ] ), @$ents ])
  0 50       0  
754             if $log->is_debug;
755 65 50       392 croak "Directory ".$dir->name." exceeds maximum entry count ($n >= 65536)"
756             if $n >= 65536;
757 65         127 $dir->file->{size}= $n * 32; # always 32 bytes per dirent
758             }
759              
760             # Pack one directory and return the buffer
761 65     65   66 sub _pack_directory($self, $dir) {
  65         60  
  65         74  
  65         71  
762 65         97 my $data= '';
763 65 100 50     183 my @special= $dir->is_root? (
764             { short11 => $self->volume_label // 'NO NAME ', flags => ATTR_VOLUME_ID },
765             ) : (
766             { short11 => ".", flags => ATTR_DIRECTORY, file => $dir->file },
767             { short11 => '..', flags => ATTR_DIRECTORY, file => $dir->parent->file },
768             );
769 65         133 for my $ent (@special, sort { lc $a->{name} cmp lc $b->{name} } $dir->entries->@*) {
  22567         29531  
770 10649         10719 my ($name, $file, $shortname, $short11)= @{$ent}{qw( name file shortname short11 )};
  10649         22160  
771 10649   66     22888 $log->tracef("encoding dirent short=%-12s long=%s cluster=%s",
      100        
772             $shortname//$short11, $name, $file && $file->cluster);
773              
774 10649 100       55045 unless (length $short11) {
775 10550         15114 my ($base, $ext)= split /\./, $shortname;
776 10550   100     20321 $short11= pack 'A8 A3', $base, ($ext//'');
777             }
778 10649         13126 $short11 =~ s/^\xE5/\x05/; # \xE5 may occur in some charsets, and needs escaped
779              
780             # Need Long-File-Name entries?
781 10649 100 100     21619 if (defined $name && $name ne $shortname) {
782             # Checksum for directory shortname, used to verify long name parts
783 36         42 my $cksum= 0;
784             $cksum= ((($cksum >> 1) | ($cksum << 7)) + $_) & 0xFF
785 36         199 for unpack 'C*', $short11;
786             # Each dirent holds up to 26 bytes (13 chars) of the long name
787 36         188 my @chars= unpack 'v*', encode('UTF-16LE', $name, Encode::FB_CROAK|Encode::LEAVE_SRC);
788             # short final chunk is padded with \0\uFFFF*
789 36 50       600 if (my $remainder= @chars % 13) {
790 36         47 push @chars, 0;
791 36         67 push @chars, (0xFFFF)x(12 - $remainder);
792             }
793 36         101 my $last= ceil(@chars/13) - 1;
794 36         77 for my $i (reverse 0..$last) {
795 37         47 my $ofs= $i*13;
796 37 100       64 my $seq= ($i + 1) | (($i == $last) ? 0x40 : 0x00);
797 37         190 $data .= pack('C v5 C C C v6 v v2',
798             $seq, # sequence and end-flag
799             @chars[$ofs .. $ofs+4], # first 5 chars
800             0x0F, 0x00, $cksum, # attr = LFN, type = 0
801             @chars[$ofs+5 .. $ofs+10], # next 6 chars
802             0, # no cluster number
803             @chars[$ofs+11 .. $ofs+12] # last 2 chars
804             );
805             }
806             }
807              
808 10649   66     21726 my $mtime= $ent->{mtime} // ($file && $file->mtime) // time;
      33        
      33        
809 10649   66     20519 my $atime= $ent->{atime} // ($file && $file->atime) // $mtime;
      33        
      33        
810 10649   66     20909 my $btime= $ent->{btime} // ($file && $file->btime) // $mtime;
      33        
      33        
811 10649         12953 my ($wdate, $wtime) = _epoch_to_fat_date_time($mtime);
812 10649         12109 my ($cdate, $ctime, $ctime_frac)= _epoch_to_fat_date_time($btime);
813 10649         12133 my ($adate) = _epoch_to_fat_date_time($atime);
814 10649   33     24801 my $flags= $ent->{flags} // ($file && $file->flags) // 0;
      66        
      50        
815             # References to the root dir are always encoded as cluster zero, even on FAT32
816             # where the root dir actually lives at a nonzero cluster.
817             # Volume label also doesn't need a cluster id. Nor do empty files.
818 10649         10520 my $cluster= 0;
819 10649 100 100     23850 if ($file && $file != $self->root->file) {
820 10614   33     14337 $cluster= $file->cluster // croak "File ".$file->name." lacks a defined cluster id";
821             }
822             # Directories always written as size = 0
823 10649 100       18661 my $size= !$file? 0 : $file->is_dir? 0 : $file->size;
    100          
824 10649         19608 $log->tracef(" with encoded size=%d cluster=%d", $size, $cluster);
825 10649         79091 $data .= pack('A11 C C C v v v v v v v V',
826             $short11, $flags, 0, #NT_reserved
827             $ctime_frac, $ctime, $cdate, $adate,
828             $cluster >> 16, $wtime, $wdate, $cluster, $size);
829             }
830 65 50       106 die "BUG: calculated dir size ".$dir->file->size." != data length ".length($data)
831             unless $dir->file->size == length $data;
832             # Dir must be padded to length of sector/cluster with entries whose name begins with \x00
833             # but that will happen automatically later as the data is appended to the file.
834 65         115 $dir->file->{data}= \$data;
835             }
836              
837             # Avoiding dependency on namespace::clean
838             delete @{Sys::Export::VFAT::}{qw(
839             carp croak confess encode decode min max ceil blessed dualvar refaddr weaken S_ISDIR S_ISREG
840             expand_stat_shorthand isa_array isa_handle isa_hash isa_int isa_pow2 write_file_extent
841             )};
842             1;
843              
844             __END__