File Coverage

blib/lib/Sys/Export/VFAT/AllocationTable.pm
Criterion Covered Total %
statement 154 166 92.7
branch 47 82 57.3
condition 19 50 38.0
subroutine 22 23 95.6
pod 13 13 100.0
total 255 334 76.3


line stmt bran cond sub pod time code
1             package Sys::Export::VFAT::AllocationTable;
2              
3             # ABSTRACT: Track which FAT clusters are used, and by what
4             our $VERSION = '0.006'; # VERSION
5              
6              
7             # Element 0 of the FAT is used for an inversion list of which sectors are allocated.
8             # It's not as good as a tree, but should perform well when the typical use case is
9             # to pack files end to end without fragmentation.
10 4     4   189297 use v5.26;
  4         12  
11 4     4   16 use warnings;
  4         6  
  4         159  
12 4     4   16 use experimental qw( signatures );
  4         5  
  4         20  
13 4     4   480 use Scalar::Util 'refaddr';
  4         6  
  4         228  
14 4     4   18 use Carp;
  4         5  
  4         156  
15 4     4   475 use Sys::Export qw( isa_int );
  4         6  
  4         21  
16 4     4   448 use Sys::Export::VFAT::Geometry qw( FAT12_MAX_CLUSTERS FAT16_MAX_CLUSTERS FAT32_MAX_CLUSTERS );
  4         7  
  4         7919  
17              
18              
19 33     33 1 6669 sub fat { $_[0]{fat} }
20 0     0   0 sub _invlist { $_[0]{_invlist} }
21 10613     10613 1 33003 sub chains { $_[0]{chains} }
22 634 100   634 1 1596 sub max_cluster_id { @_ > 1? $_[0]->set_max_cluster_id($_[1]) : $_[0]{max_cluster_id} }
23 506     506 1 967 sub max_used_cluster_id { $_[0]{_invlist}[-1] - 1 }
24              
25              
26             sub first_free_cluster {
27 5     5 1 15 my ($first_free, $max)= ($_[0]{_invlist}[1], $_[0]{max_cluster_id});
28 5 100 66     30 defined $max && $first_free > $max? undef : $first_free;
29             }
30              
31              
32 202     202 1 238 sub set_max_cluster_id($self, $val) {
  202         223  
  202         231  
  202         220  
33 202 100       270 if (defined $val) {
34 201         255 my $min= $self->max_used_cluster_id;
35 201 50 33     514 croak "Cannot set max_cluster_id less than max_used_cluster_id"
36             if defined $min && $val < $min;
37 201 50       298 croak "Cannot set max_cluster_id less than 2"
38             if $val < 2;
39             }
40 202         308 $self->{max_cluster_id}= $val;
41             }
42              
43              
44 8     8 1 19 sub free_cluster_count($self) {
  8         10  
  8         10  
45 8         22 my ($sum, $inv, $max)= (0, $self->{_invlist}, $self->{max_cluster_id});
46 8         26 for (my $i= 1; $i < $#$inv; $i += 2) {
47 2         6 $sum += $inv->[$i+1] - $inv->[$i];
48             }
49 8 100       20 $sum += $max - ($inv->[-1]-1) if defined $max;
50 8         41 return $sum;
51             }
52              
53              
54 3     3 1 5 sub get_chain($self, $cl_id) {
  3         4  
  3         4  
  3         3  
55 3         14 $self->{chains}{$cl_id};
56             }
57              
58              
59 250     250 1 158682 sub new($class, @attrs) {
  250         291  
  250         244  
  250         233  
60 250 50 33     528 my %attrs= @attrs == 1 && ref $attrs[0] eq 'HASH'? %{$attrs[0]} : @attrs;
  0         0  
61 250         320 my $max_cluster_id= delete $attrs{max_cluster_id};
62 250 0 0     329 croak "Invalid max_cluster_id"
      33        
63             unless !defined $max_cluster_id or isa_int $max_cluster_id && $max_cluster_id >= 2;
64 250 50       459 carp "Unrecognized attributes ".join(', ', keys %attrs)
65             if keys %attrs;
66              
67 250         1065 bless {
68             max_cluster_id => $max_cluster_id,
69             fat => [],
70             _invlist => [ 0, 2 ], # mark 0-1 as allocated, to remove empty-list edge cases
71             chains => {},
72             }, $class;
73             }
74              
75              
76 19736     19736 1 18227 sub alloc($self, $count) {
  19736         17971  
  19736         17573  
  19736         17259  
77 19736 50       22930 return 0 unless $count;
78 19736 50 33     23219 croak "Cluster count must be an unsigned integer"
79             unless isa_int $count && $count > 0;
80 19736         21360 my $inv= $self->{_invlist};
81 19736 50       23244 my $lim= $self->{max_cluster_id}? $self->{max_cluster_id}+1 : undef;
82             # If there are enough free sectors, this basically just removes gaps in the inversion list.
83 19736         24798 for (my $i= 1; $i < @$inv; $i+= 2) {
84 19737         19018 my ($from, $upto)= @{$inv}[$i,$i+1];
  19737         23766  
85 19737 100       21460 my $n= defined $upto? ($upto - $from) : undef;
86 19737 100 66     25018 if (!defined $n || $n >= $count) {
87 19736         17445 my @result;
88 19736 50       19960 if (!defined $n) { # allocate from final region up to max sector
    0          
89 19736 50 33     23669 last if defined $lim && $lim - $from < $count;
90 19736         28724 @result= (splice(@$inv, 1, $i, $from + $count), $from+$count);
91             }
92             elsif ($n == $count) { # result comes from exactly the gaps between other allocation
93 0         0 @result= splice(@$inv, 1, $i+1);
94             }
95             else { # result comes from partial gap
96 0         0 @result= (splice(@$inv, 1, $i-1), $from, $from+$count);
97 0         0 $inv->[1]= $from + $count;
98             }
99             # and build the cluster chain in the FAT
100 19736         18818 my $prev= 0;
101 19736         25780 for (my $j= 0; $j < @result; $j += 2) {
102 19737         27691 for ($result[$j] .. ($result[$j+1]-1)) {
103 82896 100       101401 $self->{fat}[$prev]= $_ if $prev;
104 82896         88359 $prev= $_;
105             }
106             }
107 19736         21716 $self->{fat}[$prev]= 0x0FFFFFFF;
108 19736         41101 $self->{chains}{$result[0]}{invlist}= \@result;
109 19736         41555 return $result[0];
110             }
111 1         3 $count -= $n;
112             }
113 0         0 return undef; # not enough available
114             }
115              
116              
117 8     8 1 9 sub alloc_range($self, $cluster_id, $count) {
  8         9  
  8         10  
  8         12  
  8         9  
118 8 50       13 return 0 unless $count;
119 8 50 33     20 croak "Cluster count must be an unsigned integer"
120             unless isa_int $count && $count > 0;
121 8 50 33     17 croak "Invalid cluster id '$cluster_id'"
122             unless isa_int $cluster_id && $cluster_id >= 2;
123 8         21 return $self->_alloc_range($cluster_id, $cluster_id + $count);
124             }
125              
126              
127 360     360 1 412 sub alloc_contiguous($self, $count, $align=1, $align_ofs=0) {
  360         400  
  360         385  
  360         399  
  360         380  
  360         387  
128 360 50       563 return 0 unless $count;
129 360 50 33     643 croak "Cluster count must be an unsigned integer"
130             unless isa_int $count && $count > 0;
131 360         520 my $inv= $self->{_invlist};
132 360         636 for (my $i= 1; $i < @$inv; $i+=2) {
133 437         514 my ($from, $upto)= @{$inv}[$i, $i+1];
  437         667  
134 437         486 my $start= $from;
135             # Align start addr
136 437 100       719 if ($align > 1) {
137 281         463 my $remainder= ($start - $align_ofs) & ($align-1);
138 281 100       433 $start += $align - $remainder if $remainder;
139             }
140             # Is the range large enough?
141 437 100 66     912 next if defined $upto && $upto - $start < $count;
142 360         629 return $self->_alloc_range($start, $start+$count, $i);
143             }
144 0         0 return undef;
145             }
146              
147             # add the range ($start, $lim) to an inversion list where idx is pointed at
148             # the first range that wasn't entirely before $start
149 368     368   390 sub _invlist_alloc($inv, $start, $lim, $idx=undef) {
  368         407  
  368         450  
  368         404  
  368         371  
  368         404  
150 368 100       515 unless (defined $idx) {
151 8   66     45 for ($idx= 0; $idx < $#$inv && $inv->[$idx+1] <= $start; $idx++) {}
152             # here, [idx] is less/eq start, and [idx+1] is not (or doesn't exist)
153             # If idx is even, it means start fell within an allocated range
154 8 50       17 return 0 unless $idx & 1;
155             }
156 368         461 my $from_edge= $inv->[$idx] == $start;
157             # allocating at the end
158 368 50       541 if ($idx == $#$inv) {
159             # max_cluster_id was checked by caller
160 368 100       735 $from_edge? ($inv->[$idx]= $lim)
161             : push(@$inv, $start, $lim);
162             }
163             else {
164             # does 'lim' exceed the gap between allocations?
165 0 0       0 return 0 if $lim > $inv->[$idx+1];
166 0         0 my $to_edge= $lim == $inv->[$idx+1];
167 0 0 0     0 $from_edge && $to_edge? splice(@$inv, $idx, 2)
    0          
    0          
168             : $from_edge? ($inv->[$idx]= $lim)
169             : $to_edge? ($inv->[$idx+1]= $start)
170             : splice(@$inv, $idx+1, 0, $start, $lim);
171             }
172 368         763 return 1;
173             }
174              
175 368     368   393 sub _alloc_range($self, $cl_start, $cl_lim, $invlist_idx=undef) {
  368         366  
  368         419  
  368         351  
  368         367  
  368         363  
176 368 50 33     525 return undef if $self->max_cluster_id && $cl_lim-1 > $self->max_cluster_id;
177 368 50       590 return undef unless _invlist_alloc($self->{_invlist}, $cl_start, $cl_lim, $invlist_idx);
178             # Build the cluster chain in the FAT
179             $self->{fat}[$_]= $_+1
180 368         692 for $cl_start .. $cl_lim-2;
181 368         700 $self->{fat}[$cl_lim-1]= 0x0FFFFFFF;
182             # An allocation inversion list of one segment
183 368         995 $self->{chains}{$cl_start}{invlist}= [ $cl_start, $cl_lim ];
184 368         1152 return $cl_start;
185             }
186              
187              
188 1     1 1 2 sub pack($self, $bits=undef) {
  1         2  
  1         2  
  1         2  
189 1         3 my $fat= $self->fat;
190 1   33     3 my $max= $self->max_cluster_id // $self->max_used_cluster_id;
191 1         2 my $cl_count= $max-1; # excluding clusters 0 and 1
192 1 50       4 croak "Max cluster ID exceeds FAT32 max" if $cl_count > FAT32_MAX_CLUSTERS;
193 1 50       12 carp "Truncating table to cluster id $max" if $max < $#$fat;
194 1         4 $#$fat= $max;
195 1         3 $fat->[$_]= 0x0FFFFFFF for 0,1;
196 1   50     16 $fat->[$_] //= 0 for 2..$max; # prevent warnings in pack
197 1 50       4 if ($cl_count > FAT16_MAX_CLUSTERS) {
    50          
198 0         0 return pack 'V*', @$fat;
199             } elsif ($cl_count > FAT12_MAX_CLUSTERS) {
200 0         0 return pack 'v*', @$fat;
201             } else {
202             # 12 bits per entry, pack in groups of 3 bytes, little-endian
203 1         2 my $buf= "\xFF\xFF\xFF";
204 1         3 for (my $i= 2; $i+1 <= $max; $i+= 2) {
205 19         22 my $v= ($fat->[$i] & 0xFFF) | ( ($fat->[$i+1] & 0xFFF) << 12 );
206 19         35 $buf .= pack 'vC', $v, ($v >> 16);
207             }
208 1 50       45 $buf .= pack 'v', $fat->[$max] & 0xFFF unless $max & 1;
209 1         49 return $buf;
210             }
211             }
212              
213             # Avoiding dependency on namespace::clean
214             delete @{Sys::Export::VFAT::AllocationTable::}{qw( carp confess croak refaddr isa_int )};
215             1;
216              
217             __END__