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.005'; # VERSION
4              
5 3     3   195659 use v5.26;
  3         7  
6 3     3   10 use warnings;
  3         4  
  3         115  
7 3     3   11 use experimental qw( signatures );
  3         3  
  3         22  
8 3     3   337 use Fcntl qw( S_IFDIR S_ISDIR S_ISREG );
  3         4  
  3         233  
9 3     3   13 use Scalar::Util qw( blessed dualvar refaddr weaken );
  3         3  
  3         115  
10 3     3   9 use List::Util qw( min max );
  3         12  
  3         152  
11 3     3   12 use POSIX 'ceil';
  3         4  
  3         21  
12 3     3   585 use Sys::Export::LogAny '$log';
  3         6  
  3         18  
13 3     3   962 use Encode qw( encode decode );
  3         12780  
  3         169  
14 3     3   13 use Carp;
  3         5  
  3         315  
15             our @CARP_NOT= qw( Sys::Export Sys::Export::Unix );
16             use constant {
17 3         351 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   14 use Exporter 'import';
  3         3  
  3         167  
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   612 use Sys::Export qw( isa_hash isa_array isa_handle isa_int isa_pow2 expand_stat_shorthand write_file_extent );
  3         16  
  3         16  
31 3     3   1561 use Sys::Export::VFAT::Geometry qw( FAT12 FAT16 FAT32 );
  3         6  
  3         23362  
32             require Sys::Export::VFAT::AllocationTable;
33             require Sys::Export::VFAT::File;
34             require Sys::Export::VFAT::Directory;
35              
36              
37 31     31 1 104058 sub new($class, @attrs) {
  31         56  
  31         60  
  31         48  
38             my %attrs= @attrs != 1? @attrs
39 31 50       167 : isa_hash $attrs[0]? %{$attrs[0]}
  0 50       0  
    100          
40             : isa_handle $attrs[0]? ( filehandle => $attrs[0] )
41             : ( filename => $attrs[0] );
42 31         77 my $self= bless {}, $class;
43 31         110 $self->{root}= $self->_new_dir('/', undef, undef);
44             # keep root dir separate from subdirs
45 31         97 delete $self->{_subdirs}{refaddr $self->{root}};
46             # apply other attributes
47 31         129 $self->$_($attrs{$_}) for keys %attrs;
48 31         99 $self;
49             }
50              
51             # Create dir, and store a strong reference in ->{_subdirs}
52 65     65   82 sub _new_dir($self, $name, $parent, $file) {
  65         65  
  65         86  
  65         92  
  65         63  
  65         68  
53 65         238 my $dir= Sys::Export::VFAT::Directory->new(name => $name, parent => $parent, file => $file);
54 65         196 $self->{_subdirs}{refaddr $dir}= $dir;
55 65         127 $dir;
56             }
57              
58              
59 0 0   0 1 0 sub filename { @_ > 1? ($_[0]{filename}= $_[1]) : $_[0]{filename} }
60 93 100   93 1 418 sub filehandle { @_ > 1? ($_[0]{filehandle}= $_[1]) : $_[0]{filehandle} }
61              
62              
63 21262     21262 1 21692 sub root($self) { $self->{root} }
  21262         20700  
  21262         19616  
  21262         40740  
64 212     212 1 1573 sub geometry($self) { $self->{geometry} }
  212         233  
  212         196  
  212         492  
65 33     33 1 36 sub allocation_table($self) { $self->{allocation_table} }
  33         41  
  33         30  
  33         104  
66              
67 310     310 1 392 sub volume_offset($self, @val) {
  310         317  
  310         337  
  310         299  
68 310 100       627 if ($self->{geometry}) {
69 31 50       58 croak "Geometry already decided" if @val;
70 31         99 return $self->{geometry}->volume_offset;
71             }
72 279 100       422 if (@val) {
73 20 50       49 croak "volume_offset must be a multiple of 512" if $val[0] & 511;
74 20         56 return $self->{volume_offset}= $val[0];
75             }
76 259   100     937 $self->{volume_offset} // 0
77             }
78              
79              
80 256     256 1 282 sub min_bits($self, @val) {
  256         285  
  256         251  
  256         215  
81 256 50       390 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         855 }
87              
88 34     34 1 41 sub bytes_per_sector($self, @val) {
  34         45  
  34         37  
  34         48  
89 34 50       97 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       57 if (@val) {
94 3 50       7 croak "bytes_per_sector must be a power of 2" unless isa_pow2 $val[0];
95 3         8 return $self->{bytes_per_sector}= $val[0];
96             }
97 31   100     109 $self->{bytes_per_sector} // 512
98             }
99              
100 37     37 1 69 sub sectors_per_cluster($self, @val) {
  37         39  
  37         34  
  37         42  
101 37 50       72 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       64 if (@val) {
106 3 50 33     15 croak "sectors_per_cluster must be a power of 2, and 128 or less" unless isa_pow2 $val[0] && $val[0] <= 128;
107 3         15 return $self->{sectors_per_cluster}= $val[0];
108             }
109             $self->{sectors_per_cluster}
110 34         126 }
111              
112 256     256 1 298 sub fat_count($self, @val) {
  256         325  
  256         246  
  256         236  
113 256 50       382 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       353 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         529 }
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 73 sub volume_label($self, @val) {
  62         62  
  62         75  
  62         68  
133 62 50       105 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         304 }
139              
140             # The smallest conceivable address where the data region could start
141             sub _minimum_offset_to_data {
142 1     1   8 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 36408 sub add($self, $spec) {
  10517         10434  
  10517         9575  
  10517         9500  
153 10517 100       22342 $spec= { expand_stat_shorthand($spec) }
154             if isa_array $spec;
155              
156             defined $spec->{uname} or defined $spec->{name}
157 10517 50 33     29835 or croak "Require 'uname' or 'name'";
158 10517 50       15626 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     48634 my $path= $spec->{uname} // decode('UTF-8', $spec->{name}, Encode::FB_CROAK | Encode::LEAVE_SRC);
162 10517         298882 $path =~ s,^/,,; # remove leading slash, if any
163              
164 10517         77914 my @path= grep length, split '/', $path;
165 10517         19216 my $leaf= pop @path;
166              
167             # Walk through the tree based on the case-folded path
168 10517         19675 my $parent= $self->root;
169 10517         17077 for (@path) {
170 98362         123272 my $ent= $parent->entry($_);
171 98362 100       116423 if ($ent) {
172             croak $ent->name." is not a directory, while attempting to add '$path'"
173 98329 50       135340 unless $ent->{dir};
174             } else { # Auto-create directory. Autovivication is indicated by ->{file} = undef
175 33         55 $ent= $parent->add($_, undef);
176 33 100       45 my $name= ($parent == $self->root? '' : $parent->name)."/$_";
177 33         54 weaken($ent->{dir}= $self->_new_dir($name, $parent, undef));
178             }
179 98362         108607 $parent= $ent->{dir};
180             }
181              
182             # did user supply FAT attribute bitmask?
183 10517   33     18687 my $flags= $spec->{FAT_flags} // do {
184             # readonly determined by user -write bit of 'mode'
185 10517 50 33     40538 (!($spec->{mode} & 0400)? ATTR_READONLY : 0)
    50          
186             # hidden determined by leading '.' in filename
187             | (defined $leaf && $leaf =~ /^\./? ATTR_HIDDEN : 0)
188             };
189 10517         10417 my $file;
190 10517 100       22944 if (S_ISREG($spec->{mode})) {
    50          
191             my ($size, $offset, $align, $data_ref)
192 10516         12874 = @{$spec}{qw( size device_offset device_align data )};
  10516         20276  
193 10516 100 66     25222 $data_ref= do { my $x= $data_ref; \$x }
  1573         1708  
  1573         1918  
194             if defined $data_ref && !ref $data_ref;
195 10516 50       16636 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     21462 $size //= length($$data_ref);
203             }
204             # must be a power of 2
205 10516 50 66     17207 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       14329 if (defined $offset) {
209 1   50     14 $align //= 512;
210             # must fall in the data area
211 1 50 33     3 $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         45295 $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         5 my $cur= $parent->entry($leaf);
226             croak "Attempt to add duplicate directory $leaf"
227 1 0 33     3 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       4 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         29760 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       23228 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     27828 (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         100797 $ent->{file};
265             }
266              
267              
268 31     31 1 107 sub finish($self) {
  31         36  
  31         34  
269 31         56 my $root= $self->root;
270 31         134 $log->debug('begin VFAT::finish');
271             # Find out the size of every directory, and build ->{_allocs}, ->{_dir_allocs} and ->{_special_allocs}
272 31         476 $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       108 );
280 31         72 $self->{geometry}= $geom;
281 31         56 $self->{allocation_table}= $alloc;
282 31         72 $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         157 $self->_pack_directory($_) for $root, values $self->{_subdirs}->%*;
285              
286 31         86 my $fh= $self->filehandle;
287 31 50       183 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       668 if (-s $fh < $geom->volume_offset + $geom->total_size) {
294 31         72 $log->debugf('resize output file to %s', $geom->volume_offset + $geom->total_size);
295 31 50       236 truncate($fh, $geom->volume_offset + $geom->total_size)
296             or croak "truncate: $!";
297             }
298 31         136 $self->_write_filesystem($fh, $geom, $alloc);
299 31 50       1329 unless ($self->filehandle) {
300 0 0       0 $fh->close or croak "close: $!";
301             }
302 31         207 $log->debug('end VFAT::finish');
303 31         342 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   34 sub _write_filesystem($self, $fh, $geom, $alloc) {
  31         40  
  31         32  
  31         35  
  31         32  
  31         40  
312 31 50 50     173 ($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         87 my $buf= $self->_pack_reserved_sectors;
316 31         79 my $ofs= $self->volume_offset;
317 31         68 write_file_extent($fh, $ofs, $geom->reserved_size, \$buf, 0, 'reserved sectors');
318 31         84 $ofs += $geom->reserved_size;
319             # Pack the allocation tables
320 31         72 $buf= $self->_pack_allocation_table($alloc);
321             # store a copy of this into each of the regions occupied by fats
322 31         91 for (my $i= 0; $i < $geom->fat_count; $i++) {
323 62         106 write_file_extent($fh, $ofs, $geom->fat_size, \$buf, 0, "fat table $i");
324 62         141 $ofs += $geom->fat_size;
325             }
326             # For FAT12/FAT16, write the root directory entries
327 31 100       58 if ($geom->bits < FAT32) {
328 30         66 my $rootf= $self->root->file;
329             die "BUG: mis-sized FAT16 root directory"
330             if !$rootf->size || ($rootf->size & 31)
331 30 50 33     87 || length ${$rootf->data} != $rootf->size
  30   33     54  
      33        
332             || $rootf->size > $geom->root_dir_size;
333 30         54 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         104 for my $cl (sort { $a <=> $b } keys $alloc->chains->%*) {
  114095         96531  
337 10551         20312 my ($invlist, $file)= $alloc->chains->{$cl}->@{'invlist','file'};
338 10551         20294 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       21002 $log->debugf("writing '%s' at cluster %s", $file->name, _render_invlist($invlist))
342             if $log->is_debug;
343 10551         55319 my $data_ofs= 0;
344 10551         15377 for (my $i= 0; $i < @$invlist; $i += 2) {
345 10551         10579 my ($cl_start, $cl_lim)= @{$invlist}[$i, $i+1];
  10551         15321  
346 10551         18931 my $size= ($cl_lim-$cl_start) * $geom->bytes_per_cluster;
347 10551         15992 write_file_extent($fh, $geom->get_cluster_device_offset($cl_start), $size, $data, $data_ofs);
348 10551         27405 $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 14959 sub is_valid_longname($name) {
  10556         11006  
  10556         11067  
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     53968 !!($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 184916 sub is_valid_shortname($name) {
  10602         10936  
  10602         9237  
371 10602   100     82382 !!($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 76 sub remove_invalid_shortname_chars($name, $replacement='_') {
  72         89  
  72         90  
  72         73  
392 72         94 $name =~ tr/a-z/A-Z/; # perform 'uc' but only for the ASCII range
393 72         195 $name =~ s/[^\x20\x21\x23-\x29\x2D\x30-\x39\x40-\x5A\x5E-\x7B\x7D\x7E\x80-\xFF]+/$replacement/gr;
394             }
395              
396 31     31   41 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         53 my $root= $self->root;
400 31         45 my (@offsets, @aligned, @others);
401 31         116 my %seen= ( refaddr($root) => 1 );
402 31         78 for my $dir ($root, values $self->{_subdirs}->%*) {
403 65         118 for my $ent ($dir->entries->@*) {
404             # entry may have a directory ref and not a direct file ref
405 10550   33     17088 my $file= $ent->{file} //= $ent->{dir} && $ent->{dir}->file;
      66        
406 10550 50 33     33967 next unless $file && !$seen{refaddr $file}++;
407 10550 100       9057 push @{$file->device_offset? \@offsets : $file->align? \@aligned : \@others}, $file;
  10550 100       12009  
408             }
409             }
410 31         131 $log->debugf("_optimize_geometry offsets=%d aligned=%d others=%d",
411             scalar @offsets, scalar @aligned, scalar @others);
412             # provide stable results
413 31         294 @offsets= sort { $a->device_offset <=> $b->device_offset } @offsets;
  0         0  
414 31         88 @aligned= sort { fc $a->name cmp fc $b->name } @aligned;
  18         47  
415 31         236 @others= sort { fc $a->name cmp fc $b->name } @others;
  23008         28678  
416 31         97 my $min_ofs= min(map $_->device_offset, @offsets);
417 31         78 my $max_ofs= max(map $_->device_offset + $_->size, @offsets);
418 31         74 my $max_align= max(0, map $_->align, @aligned);
419 31         81 my $root_dirent_used= $root->file->size / 32;
420 31 50 33     127 isa_int $root_dirent_used && $root_dirent_used >= 1
421             or die "BUG: root must always have one entry";
422 31         87 my $bytes_per_sector= $self->bytes_per_sector;
423 31         53 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       82 my @spc= defined $self->sectors_per_cluster? ( $self->sectors_per_cluster )
428             : (1,2,4,8,16,32,64,128);
429 31         57 cluster_size: for my $sectors_per_cluster (@spc) {
430 227         301 my $cluster_size= $sectors_per_cluster * $bytes_per_sector;
431 227 50       600 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     607 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         223 my $clusters= 0;
438 199         428 for (@offsets, @aligned, @others) {
439 20000         25839 $clusters += ceil($_->size / $cluster_size);
440             }
441 199         739 $log->tracef("with sectors_per_cluster=%d, would require at least %d clusters",
442             $sectors_per_cluster, $clusters);
443 199   100     1978 $clusters ||= 1;
444 199         229 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         384 my $align= min($cluster_size, $max_align);
450 199 100       373 if ($align) {
    100          
451             # But wait, does every device_offset meet this alignment? If not, give up.
452 133         200 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         9 my ($remainder, $prev);
464 7         9 for (@offsets) {
465 7         16 my $r= $_->device_offset & ($cluster_size-1);
466 7 50       16 if (!defined $remainder) {
    0          
467 7         8 $remainder= $r;
468 7         9 $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       16 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     280 if (!$root_clusters_added
  256         663  
486             && $clusters > Sys::Export::VFAT::Geometry::FAT16_IDEAL_MAX_CLUSTERS()
487             ) {
488 1         9 $root_clusters_added= ceil($root->file->size / $cluster_size);
489 1         4 $clusters += $root_clusters_added;
490 1         6 $log->tracef("reached FAT32 threshold, adding %s clusters for root dir", $root_clusters_added);
491             }
492 256         605 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         591 $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     2452 if (@offsets || @aligned) {
506             # tables are too large? Try again with larger clusters.
507 197 50 66     505 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     475 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         14 $clusters= ceil(($max_ofs - $geom->data_start_device_offset) / $cluster_size);
516 7         112 goto again_with_more_clusters;
517             }
518             }
519             # Now verify we have enough clusters by actually alocating them
520 249         694 my $alloc= Sys::Export::VFAT::AllocationTable->new;
521 249         313 my %assignment;
522 249 50       305 unless (eval {
523             $self->_alloc_file($geom, $alloc, $_)
524 249 100       543 for @offsets, @aligned, @others, ($geom->bits == FAT32? ($root->file) : ());
525 249         544 1
526             }) {
527 0         0 chomp($fail_reason{$sectors_per_cluster}= "$@");
528 0         0 next cluster_size;
529             }
530 249 100       519 if ($alloc->max_used_cluster_id > $geom->max_cluster_id) {
531 50         95 $clusters= $alloc->max_used_cluster_id-1;
532 50         956 goto again_with_more_clusters;
533             }
534             # Allocation worked, so clamp the allocator to this nmber of sectors
535 199         303 $alloc->max_cluster_id($geom->max_cluster_id);
536             # Is this the smallest option so far?
537 199 100 100     689 if (!$best || $best->{geom}->total_sector_count > $geom->total_sector_count) {
538 32         175 $best= { geom => $geom, alloc => $alloc, cluster_assignment => \%assignment };
539             }
540             }
541             } continue {
542             $log->tracef("%s", $fail_reason{$sectors_per_cluster})
543 199 50       597 if defined $fail_reason{$sectors_per_cluster};
544             }
545 31 50       58 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         77 $log->debugf("best cluster_size is %d", $best->{geom}->bytes_per_cluster);
551 31         338 return @{$best}{'geom','alloc'};
  31         4255  
552             }
553              
554             # reserve clusters for a file according to the align/offset needs of that file
555 20101     20101   19579 sub _alloc_file($self, $geom, $alloc, $file) {
  20101         17488  
  20101         17562  
  20101         16655  
  20101         18088  
  20101         16114  
556 20101 50       27447 my $sz= $file->size or do { carp "Attempt to allocate zero-length file"; return };
  0         0  
  0         0  
557 20101         28255 my $cl_count= POSIX::ceil($sz / $geom->bytes_per_cluster);
558 20101         19319 my $cl_start;
559 20101 100       24310 if ($file->device_offset) {
    100          
560 7         11 my ($cl, $n)= $geom->get_cluster_extent_of_device_extent($file->device_offset, $sz);
561 7   33     20 $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         558 my ($mul, $ofs)= $geom->get_cluster_alignment_of_device_alignment($file->align);
565 359   33     814 $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     25704 $cl_start= $alloc->alloc($cl_count)
569             // croak "Can't allocate $cl_count clusters";
570             }
571 20101         26011 $alloc->{chains}{$cl_start}{file}= $file;
572 20101         33964 $cl_start;
573             }
574              
575             # store the cluster and device offset into the File objects
576 31     31   31 sub _commit_allocation($self) {
  31         33  
  31         29  
577 31         69 my $alloc= $self->allocation_table;
578 31         71 my $geom= $self->geometry;
579             # Apply file cluster IDs to the File objects
580 31         85 for (values $alloc->chains->%*) {
581 10551         20230 my $file= $_->{file};
582 10551         25110 $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     9632 if 2 == @{$_->{invlist}};
  10551         24712  
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   60 sub _append_pack_args($pack, $vals, $ofs, $fields, $attrs) {
  34         36  
  34         32  
  34         38  
  34         41  
  34         41  
  34         30  
642 34         83 for (@$fields) {
643 700         1245 push @$pack, '@'.($ofs+$_->[1]).$_->[3];
644 700   66     1460 push @$vals, $attrs->{$_->[0]} // $_->[4]
      33        
645             // croak "No value supplied for $_->[0], and no default";
646             }
647             }
648              
649 31947     31947   29030 sub _epoch_to_fat_date_time($epoch) {
  31947         28987  
  31947         26407  
650 31947         185328 my @lt = localtime($epoch);
651 31947         38974 my $year = $lt[5] + 1900;
652 31947         30581 my $mon = $lt[4] + 1;
653 31947         27383 my $mday = $lt[3];
654 31947         26936 my $hour = $lt[2];
655 31947         26607 my $min = $lt[1];
656 31947         34869 my $sec = int($lt[0] / 2); # 2-second resolution
657              
658 31947 50       40455 $year = 1980 if $year < 1980;
659 31947         35168 my $fat_date = (($year - 1980) << 9) | ($mon << 5) | $mday;
660 31947         33950 my $fat_time = ($hour << 11) | ($min << 5) | $sec;
661 31947         30843 my $fat_frac = ($epoch * 100) % 200;
662 31947         56504 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   35 sub _pack_reserved_sectors($self, %attrs) {
  31         30  
  31         36  
  31         30  
668 31         35 my (@pack, @vals);
669 31         78 my $geom= $self->geometry;
670 31         62 $attrs{BPB_BytsPerSec}= $geom->bytes_per_sector;
671 31         68 $attrs{BPB_SecPerClus}= $geom->sectors_per_cluster;
672 31         72 $attrs{BPB_RsvdSecCnt}= $geom->reserved_sector_count;
673 31         79 $attrs{BPB_NumFATs}= $geom->fat_count;
674 31         54 $attrs{BPB_RootEntCnt}= $geom->root_dirent_count;
675 31   33     153 $attrs{BS_VolLab} //= $self->volume_label;
676 31   33     119 $attrs{BS_VolID} //= time & 0xFFFFFFFF;
677 31 100       58 if ($geom->bits < FAT32) {
678 30 50       52 $attrs{BPB_FATSz16}= $geom->fat_sector_count < 0x10000? $geom->fat_sector_count : 0;
679 30 50       53 $attrs{BPB_FATSz32}= $geom->fat_sector_count < 0x10000? 0 : $geom->fat_sector_count;
680 30 50       61 $attrs{BPB_TotSec16}= $geom->total_sector_count < 0x10000? $geom->total_sector_count : 0;
681 30 50       50 $attrs{BPB_TotSec32}= $geom->total_sector_count < 0x10000? 0 : $geom->total_sector_count;
682 30 100       61 $attrs{BS_FilSysType}= $geom->bits == 12? 'FAT12' : 'FAT16';
683 30         106 _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     8 $attrs{BPB_FSInfo} //= 1;
687 1   50     5 $attrs{BPB_BkBootSec} //= 2;
688 1         2 $attrs{BPB_FATSz16}= 0;
689 1         4 $attrs{BPB_FATSz32}= $geom->fat_sector_count;
690 1         4 $attrs{BPB_RootClus}= $self->root->file->cluster;
691 1         13 $attrs{BPB_TotSec16}= 0;
692 1         5 $attrs{BPB_TotSec32}= $geom->total_sector_count;
693 1         3 $attrs{BS_FilSysType}= "FAT";
694 1         19 _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     14 $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         5 _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         3 _append_pack_args(\@pack, \@vals, $bk_ofs+$fsi_ofs, \@fat32_fsinfo_fields, \%attrs);
706             }
707 31         499 pack join(' ', @pack), @vals;
708             }
709              
710             # This packs one allocation table and returns the buffer
711 31     31   33 sub _pack_allocation_table($self, $alloc) {
  31         37  
  31         34  
  31         34  
712 31         97 my $fat= [ $alloc->fat->@* ];
713 31 50 33     80 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         54 my $max= $self->geometry->max_cluster_id;
716 31         124 $#$fat= $max;
717 31         97 $fat->[$_]= 0x0FFFFFFF for 0,1;
718 31   100     26351 $fat->[$_] //= 0 for 2..$max; # prevent warnings in pack
719 31 100       81 if ($self->geometry->bits == 32) {
    100          
720 1         2669 return pack 'V*', @$fat;
721             } elsif ($self->geometry->bits == 16) {
722 1         164 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         73 for (my $i= 2; $i+1 <= $max; $i+= 2) {
727 1957         2173 my $v= ($fat->[$i] & 0xFFF) | ( ($fat->[$i+1] & 0xFFF) << 12 );
728 1957         2866 $buf .= pack 'vC', $v, ($v >> 16);
729             }
730 29 100       74 $buf .= pack 'v', $fat->[$max] & 0xFFF unless $max & 1;
731 29         158 return $buf;
732             }
733             }
734              
735             # This calculates the encoded size of one directory
736 65     65   77 sub _calc_dir_size($self, $dir) {
  65         81  
  65         85  
  65         69  
737             # If an autovivified directoy lacks a ->{file}, create it.
738 65   66     249 $dir->{file} //= Sys::Export::VFAT::File->new(name => $dir->name, flags => ATTR_DIRECTORY);
739 65         153 my $ents= $dir->entries;
740             # Need the 8.3 name in order to know whether it matches the long name
741 65         176 $dir->build_shortnames;
742             # root dir has a volume label ent, and all other dirs have '.' and '..'
743 65 100       137 my $n= @$ents + ($dir->is_root? 1 : 2);
744 65         114 for (@$ents) {
745             # Add LFN entries
746 10550 100       16784 if ($_->{name} ne $_->{shortname}) {
747 36         174 my $utf16= encode('UTF-16LE', $_->{name}, Encode::FB_CROAK|Encode::LEAVE_SRC);
748 36         3816 $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       193 [ map +( $_->{name} eq $_->{shortname}? $_->{name} : [ @{$_}{'name','shortname'} ] ), @$ents ])
  0 50       0  
754             if $log->is_debug;
755 65 50       563 croak "Directory ".$dir->name." exceeds maximum entry count ($n >= 65536)"
756             if $n >= 65536;
757 65         148 $dir->file->{size}= $n * 32; # always 32 bytes per dirent
758             }
759              
760             # Pack one directory and return the buffer
761 65     65   71 sub _pack_directory($self, $dir) {
  65         65  
  65         82  
  65         60  
762 65         84 my $data= '';
763 65 100 50     173 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         157 for my $ent (@special, sort { lc $a->{name} cmp lc $b->{name} } $dir->entries->@*) {
  22567         43695  
770 10649         13364 my ($name, $file, $shortname, $short11)= @{$ent}{qw( name file shortname short11 )};
  10649         25183  
771 10649   66     24892 $log->tracef("encoding dirent short=%-12s long=%s cluster=%s",
      100        
772             $shortname//$short11, $name, $file && $file->cluster);
773              
774 10649 100       62419 unless (length $short11) {
775 10550         17443 my ($base, $ext)= split /\./, $shortname;
776 10550   100     23303 $short11= pack 'A8 A3', $base, ($ext//'');
777             }
778 10649         15260 $short11 =~ s/^\xE5/\x05/; # \xE5 may occur in some charsets, and needs escaped
779              
780             # Need Long-File-Name entries?
781 10649 100 100     23899 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         251 for unpack 'C*', $short11;
786             # Each dirent holds up to 26 bytes (13 chars) of the long name
787 36         230 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       643 if (my $remainder= @chars % 13) {
790 36         47 push @chars, 0;
791 36         81 push @chars, (0xFFFF)x(12 - $remainder);
792             }
793 36         108 my $last= ceil(@chars/13) - 1;
794 36         76 for my $i (reverse 0..$last) {
795 37         44 my $ofs= $i*13;
796 37 100       64 my $seq= ($i + 1) | (($i == $last) ? 0x40 : 0x00);
797 37         277 $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     23194 my $mtime= $ent->{mtime} // ($file && $file->mtime) // time;
      33        
      33        
809 10649   66     23025 my $atime= $ent->{atime} // ($file && $file->atime) // $mtime;
      33        
      33        
810 10649   66     23570 my $btime= $ent->{btime} // ($file && $file->btime) // $mtime;
      33        
      33        
811 10649         13864 my ($wdate, $wtime) = _epoch_to_fat_date_time($mtime);
812 10649         13173 my ($cdate, $ctime, $ctime_frac)= _epoch_to_fat_date_time($btime);
813 10649         15343 my ($adate) = _epoch_to_fat_date_time($atime);
814 10649   33     29237 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         11140 my $cluster= 0;
819 10649 100 100     20855 if ($file && $file != $self->root->file) {
820 10614   33     16142 $cluster= $file->cluster // croak "File ".$file->name." lacks a defined cluster id";
821             }
822             # Directories always written as size = 0
823 10649 100       20622 my $size= !$file? 0 : $file->is_dir? 0 : $file->size;
    100          
824 10649         28570 $log->tracef(" with encoded size=%d cluster=%d", $size, $cluster);
825 10649         92014 $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       131 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         137 $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__