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_002'; # TRIAL VERSION
5              
6              
7 5     5   190745 use v5.28;
  5         16  
8 5     5   19 use warnings;
  5         5  
  5         196  
9 5     5   29 use experimental qw( signatures );
  5         10  
  5         23  
10 5     5   1005 use Sys::Export qw( isa_hash isa_int isa_pow2 round_up_to_pow2 round_up_to_multiple );
  5         8  
  5         26  
11 5     5   20 use Scalar::Util qw( dualvar );
  5         7  
  5         208  
12 5     5   863 use POSIX 'ceil';
  5         10527  
  5         24  
13 5     5   2487 use Carp;
  5         6  
  5         544  
14             our @CARP_NOT= qw( Sys::Export::VFAT );
15             use constant {
16 5         691 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   23 };
  5         12  
29 5     5   20 use Exporter 'import';
  5         6  
  5         12746  
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 330849 sub new($class, @attrs) {
  378         535  
  378         1208  
  378         527  
36 378 50 33     2077 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         1956 ) = 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     1077 !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     712 $volume_offset //= 0;
48 378 50 33     768 isa_int($volume_offset) && $volume_offset >= 0
49             or croak "volume_offset must be a non-negative integer";
50              
51 378   100     675 $bytes_per_sector //= 512;
52 378 50 33     622 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     567 $sectors_per_cluster //= ($bytes_per_sector >= 4096? 1 : 4096 / $bytes_per_sector);
57 378 50 33     649 isa_pow2($sectors_per_cluster) && $sectors_per_cluster <= 128
58             or croak "Invalid sectors_per_cluster $sectors_per_cluster";
59 378         503 my $cluster_size= $bytes_per_sector * $sectors_per_cluster;
60 378 50       573 $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     874 $fat_count //= 2;
65 378 50 33     570 isa_int $fat_count && 0 < $fat_count && $fat_count <= 255
      33        
66             or croak "Invalid fat_count $fat_count";
67              
68 378         1201 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         480 my $bits;
78 378 50 33     1059 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     636 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       734 unless (delete $attrs{exact_cluster_count}) {
98 378 50 66     1229 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     1067 $min_bits //= FAT12;
106 378 100       707 $bits= $cluster_count < FAT16_MIN_CLUSTERS? FAT12
    100          
107             : $cluster_count < FAT32_MIN_CLUSTERS? FAT16
108             : FAT32;
109 378 50       598 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         659 $self->{cluster_count}= $cluster_count;
120 378         834 $self->{bits}= $bits;
121              
122             # Check how many sectors are occupied by each allocation table
123 378         634 my $fat_byte_count= ( ($cluster_count + 2) * $bits + 7 ) >> 3; # round up to bytes
124 378 50       538 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         785 $fat_sector_count= int(($fat_byte_count + ($bytes_per_sector - 1)) / $bytes_per_sector);
129             }
130 378         629 $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         515 my $used_root_dirent_count= delete $attrs{used_root_dirent_count};
136 378 100       610 if ($bits < FAT32) {
137 337 100       412 if (defined $root_dirent_count) {
138 1 50 33     8 $root_dirent_count >= 1 && $root_dirent_count < 0xFFFF
139             or croak "Invalid root_dirent_count for FAT12/16";
140             } else {
141 336   100     620 $root_dirent_count= $used_root_dirent_count // 512;
142             # Round up to as many as fit in this number of sectors
143 336         643 my $remainder= ($root_dirent_count & ($self->dirent_per_sector - 1));
144 336 100       666 $root_dirent_count += ($self->dirent_per_sector - $remainder)
145             if $remainder;
146             }
147            
148 337 50 50     858 ($reserved_sector_count //= 1) == 1
149             or croak "reserved_sector_count should be 1 for FAT12/16";
150             } else {
151 41 50 50     179 ($root_dirent_count //= 0) == 0
152             or croak "root_dirent_count must be zero for FAT32";
153              
154 41   50     167 $reserved_sector_count //= 32;
155 41 50 33     87 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     967 if (defined $align_clusters && $align_clusters > $bytes_per_sector) {
161             # there's a method for this, but avoid caching things yet
162 220         353 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       353 my $align= ($cluster_size >= $align_clusters)? $align_clusters : $cluster_size;
172 220 100       407 if (my $ofs= $data_addr & ($align-1)) {
173 162         235 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       274 if ($bits < FAT32) {
176             # Reserved sectors should be 1, so expand number of root entries
177 149         225 $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         531 $self->{root_dirent_count}= $root_dirent_count;
185 378         541 $self->{reserved_sector_count}= $reserved_sector_count;
186            
187 378 50       625 carp "Unused constructor parameters: ".join(' ', keys %attrs)
188             if keys %attrs;
189 378         1404 $self;
190             }
191              
192              
193 23345     23345 1 23242 sub volume_offset($self) { $self->{volume_offset} }
  23345         23044  
  23345         21216  
  23345         35305  
194              
195 23587     23587 1 23588 sub bytes_per_sector($self) { $self->{bytes_per_sector} }
  23587         22839  
  23587         20931  
  23587         59780  
196 22073     22073 1 21399 sub sectors_per_cluster($self) { $self->{sectors_per_cluster} }
  22073         20410  
  22073         20183  
  22073         40985  
197 31338     31338 1 54311 sub bytes_per_cluster($self) { $self->{bytes_per_sector} * $self->{sectors_per_cluster} }
  31338         29457  
  31338         27849  
  31338         50540  
198 1513     1513 1 1745 sub dirent_per_sector($self) { $self->{bytes_per_sector} / 32 }
  1513         1566  
  1513         1391  
  1513         4753  
199 1     1 1 128 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 2888 sub bits($self) { $self->{bits} }
  406         415  
  406         371  
  406         1273  
203 530     530 1 805 sub reserved_sector_count($self) { $self->{reserved_sector_count} }
  530         613  
  530         527  
  530         1086  
204 62     62 1 129 sub reserved_size($self) { $self->{reserved_sector_count} * $self->bytes_per_sector }
  62         102  
  62         61  
  62         120  
205 503     503 1 691 sub fat_count($self) { $self->{fat_count} }
  503         509  
  503         487  
  503         791  
206 590     590 1 724 sub fat_sector_count($self) { $self->{fat_sector_count} }
  590         617  
  590         668  
  590         1206  
207 124     124 1 116 sub fat_size($self) { $self->{fat_sector_count} * $self->bytes_per_sector }
  124         117  
  124         125  
  124         185  
208 22592     22592 1 22682 sub cluster_count($self) { $self->{cluster_count} }
  22592         22916  
  22592         20221  
  22592         36729  
209 0     0 1 0 sub min_cluster_id($self) { 2 }
  0         0  
  0         0  
  0         0  
210 22112     22112 1 20483 sub max_cluster_id($self) { $self->cluster_count + 1 }
  22112         20066  
  22112         19617  
  22112         26563  
211 591     591 1 753 sub root_dirent_count($self) { $self->{root_dirent_count} }
  591         586  
  591         660  
  591         1105  
212 559     559 1 686 sub root_dir_sector_count($self) { ceil($self->root_dirent_count / $self->dirent_per_sector) }
  559         617  
  559         569  
  559         952  
213 60     60 1 59 sub root_dir_size($self) { $self->root_dir_sector_count * $self->bytes_per_sector }
  60         65  
  60         59  
  60         82  
214              
215 378     378 1 369 sub root_dir_start_sector($self) {
  378         353  
  378         406  
216 378         590 $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 22214 sub data_start_sector($self) {
  23539         22264  
  23539         20967  
220 23539   66     43203 $self->{data_start_sector} //= $self->root_dir_start_sector + $self->root_dir_sector_count;
221             }
222 270     270 1 300 sub data_limit_sector($self) {
  270         333  
  270         241  
223 270         321 $self->data_start_sector + $self->cluster_count * $self->sectors_per_cluster
224             }
225 1370     1370 1 1502 sub data_start_offset($self) { $self->data_start_sector * $self->bytes_per_sector }
  1370         1461  
  1370         1354  
  1370         1946  
226 270     270 1 272 sub data_limit_offset($self) { $self->data_limit_sector * $self->bytes_per_sector }
  270         261  
  270         264  
  270         400  
227 1369     1369 1 1726 sub data_start_device_offset($self) { $self->volume_offset + $self->data_start_offset }
  1369         1610  
  1369         1437  
  1369         2374  
228 270     270 1 329 sub data_limit_device_offset($self) { $self->volume_offset + $self->data_limit_offset }
  270         264  
  270         302  
  270         398  
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 645 sub total_sector_count($self) {
  526         591  
  526         545  
235 526   66     1263 $self->{total_sector_count} //= $self->data_start_sector
236             + $self->cluster_count * $self->sectors_per_cluster;
237             }
238 98     98 1 119 sub total_size($self) { $self->total_sector_count * $self->bytes_per_sector }
  98         100  
  98         92  
  98         149  
239              
240              
241 21564     21564 1 20718 sub get_cluster_start_sector($self, $cluster_id) {
  21564         19622  
  21564         19901  
  21564         18653  
242 21564 50       26595 croak "Cluster 0 and 1 are reserved" if $cluster_id < 2;
243 21564 50       25795 croak "Cluster $cluster_id beyond end of volume" if $cluster_id > $self->max_cluster_id;
244 21564         27224 return $self->data_start_sector + ($cluster_id-2) * $self->sectors_per_cluster;
245             }
246 21564     21564 1 21010 sub get_cluster_offset($self, $cluster_id) {
  21564         19922  
  21564         20249  
  21564         19627  
247 21564         27509 $self->get_cluster_start_sector($cluster_id) * $self->bytes_per_sector;
248             }
249 21557     21557 1 121995 sub get_cluster_device_offset($self, $cluster_id) {
  21557         20382  
  21557         20597  
  21557         19013  
250 21557         27298 $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         8  
  7         6  
254 7 50       12 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       13 return undef if $cluster >= $self->cluster_count;
257 7         13 return $cluster + 2;
258             }
259 7     7 1 8 sub get_cluster_of_offset($self, $offset) {
  7         8  
  7         7  
  7         8  
260 7         11 $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         10  
  7         8  
  7         6  
  7         8  
268 7         15 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       12 $self->get_cluster_offset($cl_start) == $offset
272             or croak "FAT_offset not aligned to a cluster boundary";
273 7         12 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 8 sub get_cluster_extent_of_device_extent($self, $addr, $size) {
  7         8  
  7         22  
  7         8  
  7         9  
281 7         12 $self->get_cluster_extent_of_volume_extent($addr - $self->volume_offset, $size);
282             }
283              
284              
285 587     587 1 51630 sub get_cluster_alignment_of_device_alignment($self, $align) {
  587         722  
  587         806  
  587         721  
286 587         1080 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       1197 if ($cluster_size >= $align) {
291 262 50       482 croak "Clusters are not aligned to $align"
292             if $self->data_start_device_offset & ($align-1);
293 262         640 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       597 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         527 my $cl_align= $align / $cluster_size;
302             # How many bytes away from alignment is the beginning of ficticious cluster 0?
303 325         488 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         750 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__