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.005'; # VERSION
5              
6              
7 5     5   219671 use v5.26;
  5         14  
8 5     5   19 use warnings;
  5         8  
  5         231  
9 5     5   22 use experimental qw( signatures );
  5         7  
  5         29  
10 5     5   1056 use Sys::Export qw( isa_hash isa_int isa_pow2 round_up_to_pow2 round_up_to_multiple );
  5         29  
  5         32  
11 5     5   26 use Scalar::Util qw( dualvar );
  5         8  
  5         224  
12 5     5   1118 use POSIX 'ceil';
  5         11369  
  5         32  
13 5     5   2831 use Carp;
  5         9  
  5         525  
14             our @CARP_NOT= qw( Sys::Export::VFAT );
15             use constant {
16 5         759 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   20 };
  5         8  
29 5     5   21 use Exporter 'import';
  5         8  
  5         13572  
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 375476 sub new($class, @attrs) {
  378         547  
  378         1330  
  378         518  
36 378 50 33     2315 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         1977 ) = 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     1175 !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     787 $volume_offset //= 0;
48 378 50 33     884 isa_int($volume_offset) && $volume_offset >= 0
49             or croak "volume_offset must be a non-negative integer";
50              
51 378   100     636 $bytes_per_sector //= 512;
52 378 50 33     708 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     680 $sectors_per_cluster //= ($bytes_per_sector >= 4096? 1 : 4096 / $bytes_per_sector);
57 378 50 33     533 isa_pow2($sectors_per_cluster) && $sectors_per_cluster <= 128
58             or croak "Invalid sectors_per_cluster $sectors_per_cluster";
59 378         573 my $cluster_size= $bytes_per_sector * $sectors_per_cluster;
60 378 50       602 $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     876 $fat_count //= 2;
65 378 50 33     515 isa_int $fat_count && 0 < $fat_count && $fat_count <= 255
      33        
66             or croak "Invalid fat_count $fat_count";
67              
68 378         1280 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         549 my $bits;
78 378 50 33     994 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     561 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       752 unless (delete $attrs{exact_cluster_count}) {
98 378 50 66     1246 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     1097 $min_bits //= FAT12;
106 378 100       729 $bits= $cluster_count < FAT16_MIN_CLUSTERS? FAT12
    100          
107             : $cluster_count < FAT32_MIN_CLUSTERS? FAT16
108             : FAT32;
109 378 50       597 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         615 $self->{cluster_count}= $cluster_count;
120 378         894 $self->{bits}= $bits;
121              
122             # Check how many sectors are occupied by each allocation table
123 378         595 my $fat_byte_count= ( ($cluster_count + 2) * $bits + 7 ) >> 3; # round up to bytes
124 378 50       614 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         731 $fat_sector_count= int(($fat_byte_count + ($bytes_per_sector - 1)) / $bytes_per_sector);
129             }
130 378         564 $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         535 my $used_root_dirent_count= delete $attrs{used_root_dirent_count};
136 378 100       673 if ($bits < FAT32) {
137 337 100       464 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     676 $root_dirent_count= $used_root_dirent_count // 512;
142             # Round up to as many as fit in this number of sectors
143 336         611 my $remainder= ($root_dirent_count & ($self->dirent_per_sector - 1));
144 336 100       728 $root_dirent_count += ($self->dirent_per_sector - $remainder)
145             if $remainder;
146             }
147            
148 337 50 50     940 ($reserved_sector_count //= 1) == 1
149             or croak "reserved_sector_count should be 1 for FAT12/16";
150             } else {
151 41 50 50     195 ($root_dirent_count //= 0) == 0
152             or croak "root_dirent_count must be zero for FAT32";
153              
154 41   50     150 $reserved_sector_count //= 32;
155 41 50 33     89 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     902 if (defined $align_clusters && $align_clusters > $bytes_per_sector) {
161             # there's a method for this, but avoid caching things yet
162 220         403 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       427 my $align= ($cluster_size >= $align_clusters)? $align_clusters : $cluster_size;
172 220 100       465 if (my $ofs= $data_addr & ($align-1)) {
173 162         254 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       299 if ($bits < FAT32) {
176             # Reserved sectors should be 1, so expand number of root entries
177 149         289 $root_dirent_count += $shift_n_sectors * $self->dirent_per_sector;
178             } else {
179             # Add however many reserved sectors we need
180 13         28 $reserved_sector_count += $shift_n_sectors;
181             }
182             }
183             }
184 378         527 $self->{root_dirent_count}= $root_dirent_count;
185 378         675 $self->{reserved_sector_count}= $reserved_sector_count;
186            
187 378 50       552 carp "Unused constructor parameters: ".join(' ', keys %attrs)
188             if keys %attrs;
189 378         1390 $self;
190             }
191              
192              
193 23345     23345 1 22408 sub volume_offset($self) { $self->{volume_offset} }
  23345         21902  
  23345         22462  
  23345         38619  
194              
195 23587     23587 1 25051 sub bytes_per_sector($self) { $self->{bytes_per_sector} }
  23587         23658  
  23587         21524  
  23587         64483  
196 22073     22073 1 22088 sub sectors_per_cluster($self) { $self->{sectors_per_cluster} }
  22073         21926  
  22073         19873  
  22073         44364  
197 31338     31338 1 58509 sub bytes_per_cluster($self) { $self->{bytes_per_sector} * $self->{sectors_per_cluster} }
  31338         29717  
  31338         28548  
  31338         57375  
198 1513     1513 1 1724 sub dirent_per_sector($self) { $self->{bytes_per_sector} / 32 }
  1513         1555  
  1513         1481  
  1513         5188  
199 1     1 1 127 sub dirent_per_cluster($self) { $self->{bytes_per_sector} * $self->{sectors_per_cluster} / 32 }
  1         17  
  1         3  
  1         4  
200              
201              
202 406     406 1 2863 sub bits($self) { $self->{bits} }
  406         402  
  406         389  
  406         1307  
203 530     530 1 787 sub reserved_sector_count($self) { $self->{reserved_sector_count} }
  530         581  
  530         558  
  530         1072  
204 62     62 1 65 sub reserved_size($self) { $self->{reserved_sector_count} * $self->bytes_per_sector }
  62         61  
  62         60  
  62         102  
205 503     503 1 701 sub fat_count($self) { $self->{fat_count} }
  503         489  
  503         440  
  503         932  
206 590     590 1 766 sub fat_sector_count($self) { $self->{fat_sector_count} }
  590         744  
  590         590  
  590         1398  
207 124     124 1 116 sub fat_size($self) { $self->{fat_sector_count} * $self->bytes_per_sector }
  124         130  
  124         117  
  124         187  
208 22592     22592 1 22153 sub cluster_count($self) { $self->{cluster_count} }
  22592         21234  
  22592         20066  
  22592         40978  
209 0     0 1 0 sub min_cluster_id($self) { 2 }
  0         0  
  0         0  
  0         0  
210 22112     22112 1 20970 sub max_cluster_id($self) { $self->cluster_count + 1 }
  22112         20334  
  22112         19564  
  22112         26831  
211 591     591 1 736 sub root_dirent_count($self) { $self->{root_dirent_count} }
  591         598  
  591         641  
  591         1152  
212 559     559 1 751 sub root_dir_sector_count($self) { ceil($self->root_dirent_count / $self->dirent_per_sector) }
  559         596  
  559         622  
  559         835  
213 60     60 1 62 sub root_dir_size($self) { $self->root_dir_sector_count * $self->bytes_per_sector }
  60         68  
  60         72  
  60         83  
214              
215 378     378 1 349 sub root_dir_start_sector($self) {
  378         362  
  378         372  
216 378         620 $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 22482 sub data_start_sector($self) {
  23539         22607  
  23539         21638  
220 23539   66     46973 $self->{data_start_sector} //= $self->root_dir_start_sector + $self->root_dir_sector_count;
221             }
222 270     270 1 262 sub data_limit_sector($self) {
  270         312  
  270         303  
223 270         373 $self->data_start_sector + $self->cluster_count * $self->sectors_per_cluster
224             }
225 1370     1370 1 1576 sub data_start_offset($self) { $self->data_start_sector * $self->bytes_per_sector }
  1370         1510  
  1370         1407  
  1370         2053  
226 270     270 1 262 sub data_limit_offset($self) { $self->data_limit_sector * $self->bytes_per_sector }
  270         247  
  270         250  
  270         388  
227 1369     1369 1 1872 sub data_start_device_offset($self) { $self->volume_offset + $self->data_start_offset }
  1369         1596  
  1369         1453  
  1369         2193  
228 270     270 1 315 sub data_limit_device_offset($self) { $self->volume_offset + $self->data_limit_offset }
  270         281  
  270         210  
  270         343  
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 624 sub total_sector_count($self) {
  526         474  
  526         454  
235 526   66     1545 $self->{total_sector_count} //= $self->data_start_sector
236             + $self->cluster_count * $self->sectors_per_cluster;
237             }
238 98     98 1 121 sub total_size($self) { $self->total_sector_count * $self->bytes_per_sector }
  98         99  
  98         103  
  98         145  
239              
240              
241 21564     21564 1 20047 sub get_cluster_start_sector($self, $cluster_id) {
  21564         21616  
  21564         20684  
  21564         21169  
242 21564 50       28267 croak "Cluster 0 and 1 are reserved" if $cluster_id < 2;
243 21564 50       27083 croak "Cluster $cluster_id beyond end of volume" if $cluster_id > $self->max_cluster_id;
244 21564         29280 return $self->data_start_sector + ($cluster_id-2) * $self->sectors_per_cluster;
245             }
246 21564     21564 1 22458 sub get_cluster_offset($self, $cluster_id) {
  21564         22161  
  21564         20403  
  21564         19068  
247 21564         26672 $self->get_cluster_start_sector($cluster_id) * $self->bytes_per_sector;
248             }
249 21557     21557 1 133996 sub get_cluster_device_offset($self, $cluster_id) {
  21557         20481  
  21557         21471  
  21557         20607  
250 21557         33718 $self->volume_offset + $self->get_cluster_offset($cluster_id);
251             }
252              
253 7     7 1 10 sub get_cluster_of_sector($self, $sector_idx) {
  7         8  
  7         8  
  7         26  
254 7 50       13 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       14 return undef if $cluster >= $self->cluster_count;
257 7         12 return $cluster + 2;
258             }
259 7     7 1 9 sub get_cluster_of_offset($self, $offset) {
  7         8  
  7         8  
  7         11  
260 7         12 $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 12 sub get_cluster_extent_of_volume_extent($self, $offset, $size) {
  7         9  
  7         10  
  7         9  
  7         9  
268 7         12 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       11 $self->get_cluster_offset($cl_start) == $offset
272             or croak "FAT_offset not aligned to a cluster boundary";
273 7         13 my $cl_cnt= ceil($size / $self->bytes_per_cluster);
274 7 50       15 $cl_start + $cl_cnt <= $self->max_cluster_id+1
275             or croak "byte range ($offset, $size) exceeds final cluster of volume";
276 7         20 return ($cl_start, $cl_cnt);
277             }
278              
279              
280 7     7 1 10 sub get_cluster_extent_of_device_extent($self, $addr, $size) {
  7         10  
  7         8  
  7         7  
  7         8  
281 7         15 $self->get_cluster_extent_of_volume_extent($addr - $self->volume_offset, $size);
282             }
283              
284              
285 587     587 1 58222 sub get_cluster_alignment_of_device_alignment($self, $align) {
  587         948  
  587         794  
  587         706  
286 587         1085 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       1449 if ($cluster_size >= $align) {
291 262 50       495 croak "Clusters are not aligned to $align"
292             if $self->data_start_device_offset & ($align-1);
293 262         739 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       641 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         548 my $cl_align= $align / $cluster_size;
302             # How many bytes away from alignment is the beginning of ficticious cluster 0?
303 325         477 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       692 my $cl_ofs= !$ofs_of_cl0? 0 : ($align - $ofs_of_cl0) / $cluster_size;
306 325         780 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__