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.005_002'; # TRIAL 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   198025 use v5.26;
  4         10  
11 4     4   18 use warnings;
  4         5  
  4         236  
12 4     4   20 use experimental qw( signatures );
  4         6  
  4         45  
13 4     4   467 use Scalar::Util 'refaddr';
  4         6  
  4         186  
14 4     4   32 use Carp;
  4         4  
  4         169  
15 4     4   679 use Sys::Export qw( isa_int );
  4         7  
  4         19  
16 4     4   502 use Sys::Export::VFAT::Geometry qw( FAT12_MAX_CLUSTERS FAT16_MAX_CLUSTERS FAT32_MAX_CLUSTERS );
  4         8  
  4         7633  
17              
18              
19 33     33 1 6851 sub fat { $_[0]{fat} }
20 0     0   0 sub _invlist { $_[0]{_invlist} }
21 10613     10613 1 34283 sub chains { $_[0]{chains} }
22 634 100   634 1 1638 sub max_cluster_id { @_ > 1? $_[0]->set_max_cluster_id($_[1]) : $_[0]{max_cluster_id} }
23 506     506 1 1017 sub max_used_cluster_id { $_[0]{_invlist}[-1] - 1 }
24              
25              
26             sub first_free_cluster {
27 5     5 1 17 my ($first_free, $max)= ($_[0]{_invlist}[1], $_[0]{max_cluster_id});
28 5 100 66     31 defined $max && $first_free > $max? undef : $first_free;
29             }
30              
31              
32 202     202 1 213 sub set_max_cluster_id($self, $val) {
  202         204  
  202         208  
  202         189  
33 202 100       377 if (defined $val) {
34 201         268 my $min= $self->max_used_cluster_id;
35 201 50 33     544 croak "Cannot set max_cluster_id less than max_used_cluster_id"
36             if defined $min && $val < $min;
37 201 50       322 croak "Cannot set max_cluster_id less than 2"
38             if $val < 2;
39             }
40 202         328 $self->{max_cluster_id}= $val;
41             }
42              
43              
44 8     8 1 18 sub free_cluster_count($self) {
  8         12  
  8         10  
45 8         23 my ($sum, $inv, $max)= (0, $self->{_invlist}, $self->{max_cluster_id});
46 8         34 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         55 return $sum;
51             }
52              
53              
54 3     3 1 6 sub get_chain($self, $cl_id) {
  3         3  
  3         4  
  3         3  
55 3         14 $self->{chains}{$cl_id};
56             }
57              
58              
59 250     250 1 158087 sub new($class, @attrs) {
  250         267  
  250         324  
  250         232  
60 250 50 33     561 my %attrs= @attrs == 1 && ref $attrs[0] eq 'HASH'? %{$attrs[0]} : @attrs;
  0         0  
61 250         367 my $max_cluster_id= delete $attrs{max_cluster_id};
62 250 0 0     401 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       381 carp "Unrecognized attributes ".join(', ', keys %attrs)
65             if keys %attrs;
66              
67 250         1174 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 18158 sub alloc($self, $count) {
  19736         18503  
  19736         18373  
  19736         17454  
77 19736 50       24123 return 0 unless $count;
78 19736 50 33     25351 croak "Cluster count must be an unsigned integer"
79             unless isa_int $count && $count > 0;
80 19736         22659 my $inv= $self->{_invlist};
81 19736 50       23697 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         27459 for (my $i= 1; $i < @$inv; $i+= 2) {
84 19737         19355 my ($from, $upto)= @{$inv}[$i,$i+1];
  19737         24280  
85 19737 100       23941 my $n= defined $upto? ($upto - $from) : undef;
86 19737 100 66     27477 if (!defined $n || $n >= $count) {
87 19736         18177 my @result;
88 19736 50       21666 if (!defined $n) { # allocate from final region up to max sector
    0          
89 19736 50 33     25025 last if defined $lim && $lim - $from < $count;
90 19736         31458 @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         20158 my $prev= 0;
101 19736         27025 for (my $j= 0; $j < @result; $j += 2) {
102 19737         30178 for ($result[$j] .. ($result[$j+1]-1)) {
103 82896 100       105476 $self->{fat}[$prev]= $_ if $prev;
104 82896         89132 $prev= $_;
105             }
106             }
107 19736         24170 $self->{fat}[$prev]= 0x0FFFFFFF;
108 19736         46196 $self->{chains}{$result[0]}{invlist}= \@result;
109 19736         44793 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 11 sub alloc_range($self, $cluster_id, $count) {
  8         9  
  8         9  
  8         9  
  8         10  
118 8 50       16 return 0 unless $count;
119 8 50 33     22 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         19 return $self->_alloc_range($cluster_id, $cluster_id + $count);
124             }
125              
126              
127 360     360 1 370 sub alloc_contiguous($self, $count, $align=1, $align_ofs=0) {
  360         385  
  360         397  
  360         495  
  360         401  
  360         369  
128 360 50       628 return 0 unless $count;
129 360 50 33     725 croak "Cluster count must be an unsigned integer"
130             unless isa_int $count && $count > 0;
131 360         526 my $inv= $self->{_invlist};
132 360         656 for (my $i= 1; $i < @$inv; $i+=2) {
133 437         480 my ($from, $upto)= @{$inv}[$i, $i+1];
  437         661  
134 437         503 my $start= $from;
135             # Align start addr
136 437 100       752 if ($align > 1) {
137 281         396 my $remainder= ($start - $align_ofs) & ($align-1);
138 281 100       440 $start += $align - $remainder if $remainder;
139             }
140             # Is the range large enough?
141 437 100 66     889 next if defined $upto && $upto - $start < $count;
142 360         622 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   403 sub _invlist_alloc($inv, $start, $lim, $idx=undef) {
  368         398  
  368         431  
  368         430  
  368         389  
  368         350  
150 368 100       587 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         489 my $from_edge= $inv->[$idx] == $start;
157             # allocating at the end
158 368 50       565 if ($idx == $#$inv) {
159             # max_cluster_id was checked by caller
160 368 100       764 $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         656 return 1;
173             }
174              
175 368     368   383 sub _alloc_range($self, $cl_start, $cl_lim, $invlist_idx=undef) {
  368         376  
  368         408  
  368         399  
  368         412  
  368         356  
176 368 50 33     561 return undef if $self->max_cluster_id && $cl_lim-1 > $self->max_cluster_id;
177 368 50       574 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         632 for $cl_start .. $cl_lim-2;
181 368         12720 $self->{fat}[$cl_lim-1]= 0x0FFFFFFF;
182             # An allocation inversion list of one segment
183 368         990 $self->{chains}{$cl_start}{invlist}= [ $cl_start, $cl_lim ];
184 368         1146 return $cl_start;
185             }
186              
187              
188 1     1 1 2 sub pack($self, $bits=undef) {
  1         1  
  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       3 croak "Max cluster ID exceeds FAT32 max" if $cl_count > FAT32_MAX_CLUSTERS;
193 1 50       3 carp "Truncating table to cluster id $max" if $max < $#$fat;
194 1         4 $#$fat= $max;
195 1         31 $fat->[$_]= 0x0FFFFFFF for 0,1;
196 1   50     18 $fat->[$_] //= 0 for 2..$max; # prevent warnings in pack
197 1 50       5 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         25 my $v= ($fat->[$i] & 0xFFF) | ( ($fat->[$i+1] & 0xFFF) << 12 );
206 19         45 $buf .= pack 'vC', $v, ($v >> 16);
207             }
208 1 50       5 $buf .= pack 'v', $fat->[$max] & 0xFFF unless $max & 1;
209 1         74 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__