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'; # 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   217585 use v5.26;
  4         14  
11 4     4   16 use warnings;
  4         6  
  4         200  
12 4     4   20 use experimental qw( signatures );
  4         6  
  4         27  
13 4     4   493 use Scalar::Util 'refaddr';
  4         6  
  4         266  
14 4     4   20 use Carp;
  4         5  
  4         176  
15 4     4   591 use Sys::Export qw( isa_int );
  4         8  
  4         22  
16 4     4   685 use Sys::Export::VFAT::Geometry qw( FAT12_MAX_CLUSTERS FAT16_MAX_CLUSTERS FAT32_MAX_CLUSTERS );
  4         9  
  4         7723  
17              
18              
19 33     33 1 5561 sub fat { $_[0]{fat} }
20 0     0   0 sub _invlist { $_[0]{_invlist} }
21 10613     10613 1 37174 sub chains { $_[0]{chains} }
22 634 100   634 1 1699 sub max_cluster_id { @_ > 1? $_[0]->set_max_cluster_id($_[1]) : $_[0]{max_cluster_id} }
23 506     506 1 973 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     26 defined $max && $first_free > $max? undef : $first_free;
29             }
30              
31              
32 202     202 1 227 sub set_max_cluster_id($self, $val) {
  202         192  
  202         211  
  202         221  
33 202 100       330 if (defined $val) {
34 201         266 my $min= $self->max_used_cluster_id;
35 201 50 33     505 croak "Cannot set max_cluster_id less than max_used_cluster_id"
36             if defined $min && $val < $min;
37 201 50       312 croak "Cannot set max_cluster_id less than 2"
38             if $val < 2;
39             }
40 202         345 $self->{max_cluster_id}= $val;
41             }
42              
43              
44 8     8 1 16 sub free_cluster_count($self) {
  8         13  
  8         12  
45 8         23 my ($sum, $inv, $max)= (0, $self->{_invlist}, $self->{max_cluster_id});
46 8         25 for (my $i= 1; $i < $#$inv; $i += 2) {
47 2         6 $sum += $inv->[$i+1] - $inv->[$i];
48             }
49 8 100       19 $sum += $max - ($inv->[-1]-1) if defined $max;
50 8         43 return $sum;
51             }
52              
53              
54 3     3 1 6 sub get_chain($self, $cl_id) {
  3         4  
  3         3  
  3         4  
55 3         14 $self->{chains}{$cl_id};
56             }
57              
58              
59 250     250 1 162609 sub new($class, @attrs) {
  250         268  
  250         247  
  250         273  
60 250 50 33     560 my %attrs= @attrs == 1 && ref $attrs[0] eq 'HASH'? %{$attrs[0]} : @attrs;
  0         0  
61 250         328 my $max_cluster_id= delete $attrs{max_cluster_id};
62 250 0 0     443 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       391 carp "Unrecognized attributes ".join(', ', keys %attrs)
65             if keys %attrs;
66              
67 250         1180 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 18657 sub alloc($self, $count) {
  19736         18012  
  19736         19803  
  19736         17402  
77 19736 50       23717 return 0 unless $count;
78 19736 50 33     25904 croak "Cluster count must be an unsigned integer"
79             unless isa_int $count && $count > 0;
80 19736         24748 my $inv= $self->{_invlist};
81 19736 50       23124 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         25941 for (my $i= 1; $i < @$inv; $i+= 2) {
84 19737         19886 my ($from, $upto)= @{$inv}[$i,$i+1];
  19737         24157  
85 19737 100       22710 my $n= defined $upto? ($upto - $from) : undef;
86 19737 100 66     26006 if (!defined $n || $n >= $count) {
87 19736         18370 my @result;
88 19736 50       20382 if (!defined $n) { # allocate from final region up to max sector
    0          
89 19736 50 33     25332 last if defined $lim && $lim - $from < $count;
90 19736         31022 @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         19543 my $prev= 0;
101 19736         26753 for (my $j= 0; $j < @result; $j += 2) {
102 19737         31897 for ($result[$j] .. ($result[$j+1]-1)) {
103 82896 100       110052 $self->{fat}[$prev]= $_ if $prev;
104 82896         93780 $prev= $_;
105             }
106             }
107 19736         22223 $self->{fat}[$prev]= 0x0FFFFFFF;
108 19736         45877 $self->{chains}{$result[0]}{invlist}= \@result;
109 19736         47488 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         9  
  8         9  
  8         8  
118 8 50       19 return 0 unless $count;
119 8 50 33     21 croak "Cluster count must be an unsigned integer"
120             unless isa_int $count && $count > 0;
121 8 50 33     33 croak "Invalid cluster id '$cluster_id'"
122             unless isa_int $cluster_id && $cluster_id >= 2;
123 8         23 return $self->_alloc_range($cluster_id, $cluster_id + $count);
124             }
125              
126              
127 360     360 1 420 sub alloc_contiguous($self, $count, $align=1, $align_ofs=0) {
  360         407  
  360         407  
  360         422  
  360         385  
  360         390  
128 360 50       562 return 0 unless $count;
129 360 50 33     668 croak "Cluster count must be an unsigned integer"
130             unless isa_int $count && $count > 0;
131 360         538 my $inv= $self->{_invlist};
132 360         666 for (my $i= 1; $i < @$inv; $i+=2) {
133 437         476 my ($from, $upto)= @{$inv}[$i, $i+1];
  437         688  
134 437         442 my $start= $from;
135             # Align start addr
136 437 100       651 if ($align > 1) {
137 281         352 my $remainder= ($start - $align_ofs) & ($align-1);
138 281 100       452 $start += $align - $remainder if $remainder;
139             }
140             # Is the range large enough?
141 437 100 66     990 next if defined $upto && $upto - $start < $count;
142 360         665 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   355 sub _invlist_alloc($inv, $start, $lim, $idx=undef) {
  368         409  
  368         393  
  368         390  
  368         362  
  368         340  
150 368 100       550 unless (defined $idx) {
151 8   66     54 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       36 return 0 unless $idx & 1;
155             }
156 368         444 my $from_edge= $inv->[$idx] == $start;
157             # allocating at the end
158 368 50       575 if ($idx == $#$inv) {
159             # max_cluster_id was checked by caller
160 368 100       778 $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         714 return 1;
173             }
174              
175 368     368   416 sub _alloc_range($self, $cl_start, $cl_lim, $invlist_idx=undef) {
  368         404  
  368         410  
  368         367  
  368         351  
  368         327  
176 368 50 33     588 return undef if $self->max_cluster_id && $cl_lim-1 > $self->max_cluster_id;
177 368 50       638 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         771 for $cl_start .. $cl_lim-2;
181 368         8387 $self->{fat}[$cl_lim-1]= 0x0FFFFFFF;
182             # An allocation inversion list of one segment
183 368         1093 $self->{chains}{$cl_start}{invlist}= [ $cl_start, $cl_lim ];
184 368         1154 return $cl_start;
185             }
186              
187              
188 1     1 1 2 sub pack($self, $bits=undef) {
  1         2  
  1         1  
  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         3 $fat->[$_]= 0x0FFFFFFF for 0,1;
196 1   50     39 $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         24 my $v= ($fat->[$i] & 0xFFF) | ( ($fat->[$i+1] & 0xFFF) << 12 );
206 19         34 $buf .= pack 'vC', $v, ($v >> 16);
207             }
208 1 50       4 $buf .= pack 'v', $fat->[$max] & 0xFFF unless $max & 1;
209 1         58 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__