File Coverage

blib/lib/Sys/Export/VFAT/Geometry.pm
Criterion Covered Total %
statement 245 288 85.0
branch 52 98 53.0
condition 39 85 45.8
subroutine 44 49 89.8
pod 40 40 100.0
total 420 560 75.0


line stmt bran cond sub pod time code
1             package Sys::Export::VFAT::Geometry;
2              
3             # ABSTRACT: Calculate addresses and sizes of structures within a FAT filesystem
4             our $VERSION = '0.006'; # VERSION
5              
6              
7 5     5   186901 use v5.28;
  5         13  
8 5     5   20 use warnings;
  5         7  
  5         192  
9 5     5   18 use experimental qw( signatures );
  5         6  
  5         26  
10 5     5   1001 use Sys::Export qw( isa_hash isa_int isa_pow2 round_up_to_pow2 round_up_to_multiple );
  5         6  
  5         24  
11 5     5   23 use Scalar::Util qw( dualvar );
  5         7  
  5         157  
12 5     5   921 use POSIX 'ceil';
  5         10512  
  5         22  
13 5     5   2492 use Carp;
  5         6  
  5         511  
14             our @CARP_NOT= qw( Sys::Export::VFAT );
15             use constant {
16 5         618 FAT12 => dualvar(12, "FAT12"),
17             FAT16 => dualvar(16, "FAT16"),
18             FAT32 => dualvar(32, "FAT32"),
19             FAT12_MAX_CLUSTERS => 4085-1,
20             FAT12_IDEAL_MAX_CLUSTERS => 4085-16, # docs recommend 16 away from cutoff on either side
21             FAT16_MIN_CLUSTERS => 4085,
22             FAT16_IDEAL_MIN_CLUSTERS => 4085+16,
23             FAT16_MAX_CLUSTERS => 65525-1,
24             FAT16_IDEAL_MAX_CLUSTERS => 65525-16,
25             FAT32_MIN_CLUSTERS => 65525,
26             FAT32_IDEAL_MIN_CLUSTERS => 65525+16,
27             FAT32_MAX_CLUSTERS => 0xFFFFFF5, # can't allow 0xFFFFFF7 to be allocatable ID
28 5     5   22 };
  5         7  
29 5     5   22 use Exporter 'import';
  5         5  
  5         14325  
30             our @EXPORT_OK= qw( FAT12 FAT16 FAT32 FAT12_MAX_CLUSTERS FAT12_IDEAL_MAX_CLUSTERS
31             FAT16_MIN_CLUSTERS FAT16_IDEAL_MIN_CLUSTERS FAT16_MAX_CLUSTERS FAT16_IDEAL_MAX_CLUSTERS
32             FAT32_MIN_CLUSTERS FAT32_IDEAL_MIN_CLUSTERS FAT32_MAX_CLUSTERS );
33              
34              
35 378     378 1 328679 sub new($class, @attrs) {
  378         431  
  378         1079  
  378         418  
36 378 50 33     1862 my %attrs= @attrs == 1 && isa_hash $attrs[0]? %{$attrs[0]} : @attrs;
  0         0  
37             my ($bytes_per_sector, $sectors_per_cluster, $fat_count, $reserved_sector_count,
38             $fat_sector_count, $root_dirent_count, $cluster_count, $total_sector_count,
39             $min_bits, $volume_offset, $align_clusters
40 378         1681 ) = delete @attrs{qw(
41             bytes_per_sector sectors_per_cluster fat_count reserved_sector_count
42             fat_sector_count root_dirent_count cluster_count total_sector_count
43             min_bits volume_offset align_clusters
44             )};
45 378 50 66     1061 !defined $align_clusters or isa_pow2($align_clusters)
46             or croak "align_clusters must be a power of 2 (was $align_clusters)";
47 378   100     600 $volume_offset //= 0;
48 378 50 33     725 isa_int($volume_offset) && $volume_offset >= 0
49             or croak "volume_offset must be a non-negative integer";
50              
51 378   100     615 $bytes_per_sector //= 512;
52 378 50 33     562 isa_pow2($bytes_per_sector) && 512 <= $bytes_per_sector && $bytes_per_sector <= 4096
      33        
53             or croak "Invalid bytes_per_sector $bytes_per_sector";
54              
55             # Default sectors_per_cluster to whatever makes 4K
56 378 50 66     566 $sectors_per_cluster //= ($bytes_per_sector >= 4096? 1 : 4096 / $bytes_per_sector);
57 378 50 33     508 isa_pow2($sectors_per_cluster) && $sectors_per_cluster <= 128
58             or croak "Invalid sectors_per_cluster $sectors_per_cluster";
59 378         449 my $cluster_size= $bytes_per_sector * $sectors_per_cluster;
60 378 50       576 $cluster_size <= 32*1024
61             or carp "Warning: bytes_per_sector * sectors_per_cluster > 32KiB which is not valid for some drivers";
62              
63             # Default fat_count to 2 unless specified otherwise
64 378   100     814 $fat_count //= 2;
65 378 50 33     482 isa_int $fat_count && 0 < $fat_count && $fat_count <= 255
      33        
66             or croak "Invalid fat_count $fat_count";
67              
68 378         1103 my $self= bless {
69             bytes_per_sector => $bytes_per_sector,
70             sectors_per_cluster => $sectors_per_cluster,
71             fat_count => $fat_count,
72             volume_offset => $volume_offset,
73             };
74            
75             # From here down, we are either determining cluster_count from other properties,
76             # or deriving other properties from cluster_count.
77 378         434 my $bits;
78 378 50 33     920 if (defined $reserved_sector_count && defined $fat_sector_count
    50 33        
      0        
79             && defined $root_dirent_count && defined $total_sector_count
80             ) {
81             # All main properties of the geometry are defined.
82 0         0 my $root_sector_count= int(($root_dirent_count + ($self->dirent_per_sector-1)) / $self->dirent_per_sector);
83 0         0 my $data_sectors= $total_sector_count - $reserved_sector_count - $fat_count * $fat_sector_count - $root_sector_count;
84 0         0 my $calc_cluster_count= int($data_sectors / $sectors_per_cluster);
85 0 0 0     0 croak "Supplied cluster_count disagrees with computed value"
86             if defined $cluster_count && $cluster_count != $calc_cluster_count;
87 0   0     0 $cluster_count //= $calc_cluster_count;
88 0 0       0 $bits= $cluster_count < FAT16_MIN_CLUSTERS? FAT12
    0          
89             : $cluster_count < FAT32_MIN_CLUSTERS? FAT16
90             : FAT32;
91             }
92             elsif (defined $cluster_count) {
93 378 50 33     475 isa_int $cluster_count && $cluster_count > 0
94             or croak "Invalid cluster_count '$cluster_count'";
95             # FAT docs recommend avoiding numbers near the boundary of FAT12/FAT16/FAT32 to avoid
96             # other people's math errors. But, allow the caller to disable this adjustment.
97 378 50       660 unless (delete $attrs{exact_cluster_count}) {
98 378 50 66     1133 if ($cluster_count >= FAT12_IDEAL_MAX_CLUSTERS && $cluster_count < FAT16_IDEAL_MIN_CLUSTERS) {
    50 66        
99 0         0 $cluster_count= FAT16_IDEAL_MIN_CLUSTERS;
100             } elsif ($cluster_count >= FAT16_IDEAL_MAX_CLUSTERS && $cluster_count < FAT32_IDEAL_MIN_CLUSTERS) {
101 0         0 $cluster_count= FAT32_IDEAL_MIN_CLUSTERS;
102             }
103             }
104             # These are the official boundary numbers that determine the filesystem type
105 378   50     1086 $min_bits //= FAT12;
106 378 100       628 $bits= $cluster_count < FAT16_MIN_CLUSTERS? FAT12
    100          
107             : $cluster_count < FAT32_MIN_CLUSTERS? FAT16
108             : FAT32;
109 378 50       527 if ($bits < $min_bits) {
110 0         0 $bits= $min_bits;
111             # Increase to the minimum number of clusters if a specific number of bits
112             # was requested.
113 0 0       0 $cluster_count= ($bits == FAT16)? FAT16_IDEAL_MIN_CLUSTERS : FAT32_IDEAL_MIN_CLUSTERS;
114             }
115             }
116             else {
117 0         0 croak "Not enough attributes supplied to determine geometry";
118             }
119 378         524 $self->{cluster_count}= $cluster_count;
120 378         681 $self->{bits}= $bits;
121              
122             # Check how many sectors are occupied by each allocation table
123 378         562 my $fat_byte_count= ( ($cluster_count + 2) * $bits + 7 ) >> 3; # round up to bytes
124 378 50       482 if (defined $fat_sector_count) {
125 0 0       0 $fat_sector_count * $bytes_per_sector >= $fat_byte_count
126             or croak "Invalid fat_sector_count, smaller than $fat_byte_count bytes";
127             } else {
128 378         646 $fat_sector_count= int(($fat_byte_count + ($bytes_per_sector - 1)) / $bytes_per_sector);
129             }
130 378         671 $self->{fat_sector_count}= $fat_sector_count;
131              
132             # Check how many sectors are occupied by root directory entries
133             # For fat12/16, The FAT spec document suggests 512 as a good default
134             # Allow the user to supply the actual number of root entries and then we round that.
135 378         471 my $used_root_dirent_count= delete $attrs{used_root_dirent_count};
136 378 100       519 if ($bits < FAT32) {
137 337 100       422 if (defined $root_dirent_count) {
138 1 50 33     6 $root_dirent_count >= 1 && $root_dirent_count < 0xFFFF
139             or croak "Invalid root_dirent_count for FAT12/16";
140             } else {
141 336   100     567 $root_dirent_count= $used_root_dirent_count // 512;
142             # Round up to as many as fit in this number of sectors
143 336         595 my $remainder= ($root_dirent_count & ($self->dirent_per_sector - 1));
144 336 100       593 $root_dirent_count += ($self->dirent_per_sector - $remainder)
145             if $remainder;
146             }
147            
148 337 50 50     893 ($reserved_sector_count //= 1) == 1
149             or croak "reserved_sector_count should be 1 for FAT12/16";
150             } else {
151 41 50 50     140 ($root_dirent_count //= 0) == 0
152             or croak "root_dirent_count must be zero for FAT32";
153              
154 41   50     119 $reserved_sector_count //= 32;
155 41 50 33     65 isa_int $reserved_sector_count && $reserved_sector_count >= 2
156             or croak "reserved_sector_count must be greater than 2 for FAT32";
157             }
158              
159             # If caller requested alignment of clusters, figure that out
160 378 100 100     812 if (defined $align_clusters && $align_clusters > $bytes_per_sector) {
161             # there's a method for this, but avoid caching things yet
162 220         410 my $data_addr= $volume_offset + $bytes_per_sector * (
163             $reserved_sector_count
164             + ($fat_count*$fat_sector_count)
165             + ceil($root_dirent_count / $self->dirent_per_sector)
166             );
167             # If the cluster size is greater or equal to the requested alignment, ensure the
168             # data start falls on that boundary.
169             # If the cluster size is smaller than the requested alignment, ensure the data start
170             # falls on a cluster boundary so that some number of clusters will equal the alignment.
171 220 100       448 my $align= ($cluster_size >= $align_clusters)? $align_clusters : $cluster_size;
172 220 100       429 if (my $ofs= $data_addr & ($align-1)) {
173 162         242 my $shift_n_sectors= ($align - $ofs) / $bytes_per_sector;
174             #say sprintf "# remainder=0x%X, cluster_size=$cluster_size align_clusters=$align_clusters $align-$ofs=".($align-$ofs)." shift %d sectors", $ofs, $shift_n_sectors;
175 162 100       263 if ($bits < FAT32) {
176             # Reserved sectors should be 1, so expand number of root entries
177 149         227 $root_dirent_count += $shift_n_sectors * $self->dirent_per_sector;
178             } else {
179             # Add however many reserved sectors we need
180 13         26 $reserved_sector_count += $shift_n_sectors;
181             }
182             }
183             }
184 378         560 $self->{root_dirent_count}= $root_dirent_count;
185 378         542 $self->{reserved_sector_count}= $reserved_sector_count;
186            
187 378 50       545 carp "Unused constructor parameters: ".join(' ', keys %attrs)
188             if keys %attrs;
189 378         1243 $self;
190             }
191              
192              
193 23345     23345 1 20332 sub volume_offset($self) { $self->{volume_offset} }
  23345         20458  
  23345         19857  
  23345         33912  
194              
195 23587     23587 1 22631 sub bytes_per_sector($self) { $self->{bytes_per_sector} }
  23587         21042  
  23587         20333  
  23587         56505  
196 22073     22073 1 20789 sub sectors_per_cluster($self) { $self->{sectors_per_cluster} }
  22073         19493  
  22073         20078  
  22073         39369  
197 31338     31338 1 50716 sub bytes_per_cluster($self) { $self->{bytes_per_sector} * $self->{sectors_per_cluster} }
  31338         31392  
  31338         27703  
  31338         49359  
198 1513     1513 1 1592 sub dirent_per_sector($self) { $self->{bytes_per_sector} / 32 }
  1513         1411  
  1513         1320  
  1513         4111  
199 1     1 1 124 sub dirent_per_cluster($self) { $self->{bytes_per_sector} * $self->{sectors_per_cluster} / 32 }
  1         1  
  1         2  
  1         3  
200              
201              
202 406     406 1 2693 sub bits($self) { $self->{bits} }
  406         403  
  406         343  
  406         1253  
203 530     530 1 827 sub reserved_sector_count($self) { $self->{reserved_sector_count} }
  530         570  
  530         484  
  530         906  
204 62     62 1 75 sub reserved_size($self) { $self->{reserved_sector_count} * $self->bytes_per_sector }
  62         82  
  62         57  
  62         95  
205 503     503 1 638 sub fat_count($self) { $self->{fat_count} }
  503         463  
  503         436  
  503         764  
206 590     590 1 780 sub fat_sector_count($self) { $self->{fat_sector_count} }
  590         667  
  590         577  
  590         1192  
207 124     124 1 111 sub fat_size($self) { $self->{fat_sector_count} * $self->bytes_per_sector }
  124         122  
  124         107  
  124         169  
208 22592     22592 1 20225 sub cluster_count($self) { $self->{cluster_count} }
  22592         19706  
  22592         18908  
  22592         35809  
209 0     0 1 0 sub min_cluster_id($self) { 2 }
  0         0  
  0         0  
  0         0  
210 22112     22112 1 20385 sub max_cluster_id($self) { $self->cluster_count + 1 }
  22112         19455  
  22112         18466  
  22112         24752  
211 591     591 1 739 sub root_dirent_count($self) { $self->{root_dirent_count} }
  591         593  
  591         651  
  591         926  
212 559     559 1 657 sub root_dir_sector_count($self) { ceil($self->root_dirent_count / $self->dirent_per_sector) }
  559         515  
  559         603  
  559         788  
213 60     60 1 60 sub root_dir_size($self) { $self->root_dir_sector_count * $self->bytes_per_sector }
  60         52  
  60         63  
  60         74  
214              
215 378     378 1 366 sub root_dir_start_sector($self) {
  378         348  
  378         304  
216 378         510 $self->reserved_sector_count + $self->fat_count * $self->fat_sector_count
217             }
218 0     0 1 0 sub root_dir_offset($self) { $self->root_dir_start_sector * $self->bytes_per_sector }
  0         0  
  0         0  
  0         0  
219 23539     23539 1 20707 sub data_start_sector($self) {
  23539         20921  
  23539         21470  
220 23539   66     40482 $self->{data_start_sector} //= $self->root_dir_start_sector + $self->root_dir_sector_count;
221             }
222 270     270 1 255 sub data_limit_sector($self) {
  270         232  
  270         239  
223 270         329 $self->data_start_sector + $self->cluster_count * $self->sectors_per_cluster
224             }
225 1370     1370 1 1232 sub data_start_offset($self) { $self->data_start_sector * $self->bytes_per_sector }
  1370         1325  
  1370         1253  
  1370         1782  
226 270     270 1 324 sub data_limit_offset($self) { $self->data_limit_sector * $self->bytes_per_sector }
  270         295  
  270         219  
  270         418  
227 1369     1369 1 1636 sub data_start_device_offset($self) { $self->volume_offset + $self->data_start_offset }
  1369         1365  
  1369         1208  
  1369         1730  
228 270     270 1 303 sub data_limit_device_offset($self) { $self->volume_offset + $self->data_limit_offset }
  270         259  
  270         237  
  270         368  
229              
230 0     0 1 0 sub data_sector_count($self) {
  0         0  
  0         0  
231 0         0 $self->total_sector_count - $self->data_start_sector;
232             }
233              
234 526     526 1 690 sub total_sector_count($self) {
  526         539  
  526         441  
235 526   66     1084 $self->{total_sector_count} //= $self->data_start_sector
236             + $self->cluster_count * $self->sectors_per_cluster;
237             }
238 98     98 1 101 sub total_size($self) { $self->total_sector_count * $self->bytes_per_sector }
  98         144  
  98         99  
  98         126  
239              
240              
241 21564     21564 1 18732 sub get_cluster_start_sector($self, $cluster_id) {
  21564         19916  
  21564         19917  
  21564         19160  
242 21564 50       25905 croak "Cluster 0 and 1 are reserved" if $cluster_id < 2;
243 21564 50       25244 croak "Cluster $cluster_id beyond end of volume" if $cluster_id > $self->max_cluster_id;
244 21564         25614 return $self->data_start_sector + ($cluster_id-2) * $self->sectors_per_cluster;
245             }
246 21564     21564 1 19023 sub get_cluster_offset($self, $cluster_id) {
  21564         20272  
  21564         21019  
  21564         19818  
247 21564         28103 $self->get_cluster_start_sector($cluster_id) * $self->bytes_per_sector;
248             }
249 21557     21557 1 110194 sub get_cluster_device_offset($self, $cluster_id) {
  21557         19593  
  21557         20159  
  21557         19365  
250 21557         25096 $self->volume_offset + $self->get_cluster_offset($cluster_id);
251             }
252              
253 7     7 1 8 sub get_cluster_of_sector($self, $sector_idx) {
  7         8  
  7         6  
  7         7  
254 7 50       11 return undef if $sector_idx < $self->data_start_sector;
255 7         11 my $cluster= int(($sector_idx - $self->data_start_sector) / $self->sectors_per_cluster);
256 7 50       9 return undef if $cluster >= $self->cluster_count;
257 7         13 return $cluster + 2;
258             }
259 7     7 1 7 sub get_cluster_of_offset($self, $offset) {
  7         7  
  7         8  
  7         6  
260 7         13 $self->get_cluster_of_sector(int($offset / $self->bytes_per_sector));
261             }
262 0     0 1 0 sub get_cluster_of_device_offset($self, $addr) {
  0         0  
  0         0  
  0         0  
263 0         0 $self->get_cluster_of_offset($addr - $self->volume_offset);
264             }
265              
266              
267 7     7 1 8 sub get_cluster_extent_of_volume_extent($self, $offset, $size) {
  7         7  
  7         8  
  7         8  
  7         8  
268 7         11 my $cl_start= $self->get_cluster_of_offset($offset);
269 7 50       14 $cl_start
270             or croak "Offset $offset falls outside of cluster data region";
271 7 50       10 $self->get_cluster_offset($cl_start) == $offset
272             or croak "FAT_offset not aligned to a cluster boundary";
273 7         11 my $cl_cnt= ceil($size / $self->bytes_per_cluster);
274 7 50       13 $cl_start + $cl_cnt <= $self->max_cluster_id+1
275             or croak "byte range ($offset, $size) exceeds final cluster of volume";
276 7         15 return ($cl_start, $cl_cnt);
277             }
278              
279              
280 7     7 1 8 sub get_cluster_extent_of_device_extent($self, $addr, $size) {
  7         10  
  7         8  
  7         7  
  7         7  
281 7         11 $self->get_cluster_extent_of_volume_extent($addr - $self->volume_offset, $size);
282             }
283              
284              
285 587     587 1 45763 sub get_cluster_alignment_of_device_alignment($self, $align) {
  587         691  
  587         753  
  587         600  
286 587         815 my $cluster_size= $self->bytes_per_cluster;
287             # If the cluster size is greater or equal to the requested alignment, verify that the
288             # data start (plus the volume offset that was implicitly added to every volume address)
289             # meets that alignment. If so, every cluster is aligned and the return value is (1,0)
290 587 100       1122 if ($cluster_size >= $align) {
291 262 50       412 croak "Clusters are not aligned to $align"
292             if $self->data_start_device_offset & ($align-1);
293 262         537 return (1,0);
294             }
295             # Otherwise, make sure the data_start (plus implied volume offset) is aligned to
296             # cluster_size, so then some multiple of clusters will reach the requested alignment.
297 325 50       518 croak "Clusters are not aligned to $cluster_size"
298             if $self->data_start_device_offset & ($cluster_size-1);
299             # The cluster alignment will be whatever multiple of clusters equals the byte alignment.
300             # This will be at least 2.
301 325         453 my $cl_align= $align / $cluster_size;
302             # How many bytes away from alignment is the beginning of ficticious cluster 0?
303 325         444 my $ofs_of_cl0= ($self->data_start_device_offset - ($cluster_size*2)) & ($align-1);
304             # If not aligned, add the rest of the distance to the next alignment.
305 325 100       552 my $cl_ofs= !$ofs_of_cl0? 0 : ($align - $ofs_of_cl0) / $cluster_size;
306 325         670 return ($cl_align, $cl_ofs);
307             }
308              
309              
310             sub unpack {
311 0     0 1   my $class= shift;
312 0 0         my $buf_ref= ref $_[0] eq 'SCALAR'? $_[0] : \$_[0];
313 0 0         my %attrs= ref $_[1] eq 'HASH'? %{$_[1]} : @_[1..$#_];
  0            
314 0 0         length($$buf_ref) >= 512 or croak "Pass at least the entire first sector to 'unpack'";
315             # According to the official spec, the only way to know whether you have FAT32 or FAT16
316             # is to calculate the count of clusters available in the data region, which this module
317             # implements in the constructor. However, in order to unpack all of the fields of
318             # sector0, you have to know whether it is FAT16 or FAT32 because FAT32 moves some of the
319             # fields. But you need those extended fields to calculate whether it is FAT32 or not...
320             # It's sort of a bullshit circular dependency when clearly you could use the
321             # BPB_RootEntCnt to know whether it was FAT32 or not, since FAT32 will *always* be 0 and
322             # the previous generations *can't* be 0.
323             # Anyway, instead of uniform single-pass field unpacking, we get this:
324 0           state %fields= qw(
325             bytes_per_sector @11v
326             sectors_per_cluster @13C
327             reserved_sector_count @14v
328             fat_count @16C
329             root_dirent_count @17v
330             fat_sector_count @22v
331             fat_sector_count32 @36V
332             total_sector_count @19v
333             total_sector_count32 @32V
334             );
335 0           state @fields= keys %fields;
336 0           state $packstr= join ' ', values %fields;
337              
338 0           @attrs{@fields}= unpack $packstr, $$buf_ref;
339 0           for (qw( fat_sector_count total_sector_count )) {
340 0           my $_32= delete $attrs{$_.'32'};
341 0   0       $attrs{$_} ||= $_32;
342             }
343 0           return $class->new(%attrs);
344             }
345              
346             # Avoiding dependency on namespace::clean
347             delete @{Sys::Export::VFAT::Geometry::}{qw(
348             carp confess croak ceil dualvar
349             isa_hash isa_int isa_pow2 round_up_to_multiple round_up_to_pow2
350             )};
351             1;
352              
353             __END__