File Coverage

blib/lib/Archive/Ar.pm
Criterion Covered Total %
statement 279 312 89.4
branch 87 144 60.4
condition 32 49 65.3
subroutine 39 43 90.7
pod 21 23 91.3
total 458 571 80.2


line stmt bran cond sub pod time code
1             ###########################################################
2             # Archive::Ar - Pure perl module to handle ar achives
3             #
4             # Copyright 2003 - Jay Bonci
5             # Copyright 2014 - John Bazik
6             # Licensed under the same terms as perl itself
7             #
8             ###########################################################
9             package Archive::Ar;
10              
11 15     15   780182 use base qw(Exporter);
  15         41  
  15         2420  
12             our @EXPORT_OK = qw(COMMON BSD GNU);
13              
14 15     15   92 use strict;
  15         31  
  15         564  
15 15     15   96 use File::Spec;
  15         24  
  15         337  
16 15     15   29097 use Time::Local;
  15         37545  
  15         1084  
17 15     15   116 use Carp qw(carp longmess);
  15         32  
  15         955  
18              
19 15     15   78 use vars qw($VERSION);
  15         33  
  15         1026  
20             $VERSION = '2.02';
21              
22 15   33 15   76 use constant CAN_CHOWN => ($> == 0 and $^O ne 'MacOS' and $^O ne 'MSWin32');
  15         30  
  15         1128  
23              
24 15     15   74 use constant ARMAG => "!\n";
  15         26  
  15         822  
25 15     15   79 use constant SARMAG => length(ARMAG);
  15         31  
  15         850  
26 15     15   244 use constant ARFMAG => "`\n";
  15         28  
  15         1931  
27 15     15   76 use constant AR_EFMT1 => "#1/";
  15         26  
  15         719  
28              
29 15     15   72 use constant COMMON => 1;
  15         29  
  15         616  
30 15     15   73 use constant BSD => 2;
  15         33  
  15         862  
31 15     15   73 use constant GNU => 3;
  15         31  
  15         1194  
32              
33             my $has_io_string;
34             BEGIN {
35 15   50 15   56 $has_io_string = eval {
36             require IO::String;
37             IO::String->import();
38             1;
39             } || 0;
40             }
41              
42             sub new {
43 26     26 1 50024 my $class = shift;
44 26         60 my $file = shift;
45 26   100     196 my $opts = shift || 0;
46 26         87 my $self = bless {}, $class;
47 26 50       295 my $defopts = {
48             chmod => 1,
49             chown => 1,
50             same_perms => ($> == 0) ? 1:0,
51             symbols => undef,
52             };
53 26 50       138 $opts = {warn => $opts} unless ref $opts;
54              
55 26         107 $self->clear();
56 26         91 $self->{opts} = {(%$defopts, %{$opts})};
  26         129  
57 26 100       101 if ($file) {
58 10 100       37 return unless $self->read($file);
59             }
60 23         111 return $self;
61             }
62              
63             sub set_opt {
64 1     1 1 379 my $self = shift;
65 1         2 my $name = shift;
66 1         2 my $val = shift;
67              
68 1         3 $self->{opts}->{$name} = $val;
69             }
70              
71             sub get_opt {
72 3     3 1 348 my $self = shift;
73 3         3 my $name = shift;
74              
75 3         18 return $self->{opts}->{$name};
76             }
77              
78             sub type {
79 3     3 1 25 return shift->{type};
80             }
81              
82             sub clear {
83 41     41 1 68 my $self = shift;
84              
85 41         215 $self->{names} = [];
86 41         104 $self->{files} = {};
87 41         523 $self->{type} = undef;
88             }
89              
90             sub read {
91 7     7 1 25 my $self = shift;
92 7         11 my $file = shift;
93              
94 7         41 my $fh = $self->_get_handle($file);
95 7         28 local $/ = undef;
96 7         183 my $data = <$fh>;
97 7         75 close $fh;
98            
99 7         31 return $self->read_memory($data);
100             }
101              
102             sub read_memory {
103 15     15 1 967 my $self = shift;
104 15         26 my $data = shift;
105              
106 15         48 $self->clear();
107 15 50       66 return unless $self->_parse($data);
108 15         140 return length($data);
109             }
110              
111             sub contains_file {
112 0     0 1 0 my $self = shift;
113 0         0 my $filename = shift;
114              
115 0 0       0 return unless defined $filename;
116 0         0 return exists $self->{files}->{$filename};
117             }
118              
119             sub extract {
120 1     1 1 3 my $self = shift;
121              
122 1 50       4 for my $filename (@_ ? @_ : @{$self->{names}}) {
  1         6  
123 2 50       6 $self->extract_file($filename) or return;
124             }
125 1         6 return 1;
126             }
127              
128             sub extract_file {
129 2     2 0 3 my $self = shift;
130 2         3 my $filename = shift;
131 2   33     11 my $target = shift || $filename;
132              
133 2         6 my $meta = $self->{files}->{$filename};
134 2 50       5 return $self->_error("$filename: not in archive") unless $meta;
135 2 50       183 open my $fh, '>', $target or return $self->_error("$target: $!");
136 2         6 binmode $fh;
137 2 50       91 syswrite $fh, $meta->{data} or return $self->_error("$filename: $!");
138 2 50       26 close $fh or return $self->_error("$filename: $!");
139 2 50       8 if (CAN_CHOWN && $self->{opts}->{chown}) {
140 2 50       53 chown $meta->{uid}, $meta->{gid}, $filename or
141             return $self->_error("$filename: $!");
142             }
143 2 50       7 if ($self->{opts}->{chmod}) {
144 2         5 my $mode = $meta->{mode};
145 2 50       12 unless ($self->{opts}->{same_perms}) {
146 0         0 $mode &= ~(oct(7000) | (umask | 0));
147             }
148 2 50       38 chmod $mode, $filename or return $self->_error("$filename: $!");
149             }
150 2 50       46 utime $meta->{date}, $meta->{date}, $filename or
151             return $self->_error("$filename: $!");
152 2         16 return 1;
153             }
154              
155             sub rename {
156 2     2 1 4 my $self = shift;
157 2         5 my $filename = shift;
158 2         3 my $target = shift;
159              
160 2 50       10 if ($self->{files}->{$filename}) {
161 2         6 $self->{files}->{$target} = $self->{files}->{$filename};
162 2         6 delete $self->{files}->{$filename};
163 2         3 for (@{$self->{names}}) {
  2         5  
164 5 100       12 if ($_ eq $filename) {
165 2         4 $_ = $target;
166 2         5 last;
167             }
168             }
169             }
170             }
171              
172             sub chmod {
173 0     0 1 0 my $self = shift;
174 0         0 my $filename = shift;
175 0         0 my $mode = shift; # octal string or numeric
176              
177 0 0       0 return unless $self->{files}->{$filename};
178 0 0       0 $self->{files}->{$filename}->{mode} =
179             $mode + 0 eq $mode ? $mode : oct($mode);
180 0         0 return 1;
181             }
182              
183             sub chown {
184 0     0 1 0 my $self = shift;
185 0         0 my $filename = shift;
186 0         0 my $uid = shift;
187 0         0 my $gid = shift;
188              
189 0 0       0 return unless $self->{files}->{$filename};
190 0 0       0 $self->{files}->{$filename}->{uid} = $uid if $uid >= 0;
191 0 0 0     0 $self->{files}->{$filename}->{gid} = $gid if defined $gid && $gid >= 0;
192 0         0 return 1;
193             }
194              
195             sub remove {
196 2     2 1 983 my $self = shift;
197 2 100       8 my $files = ref $_[0] ? shift : \@_;
198              
199 2         3 my $nfiles_orig = scalar @{$self->{names}};
  2         4  
200              
201 2         5 for my $file (@$files) {
202 4 50       10 next unless $file;
203 4 50       11 if (exists($self->{files}->{$file})) {
204 4         19 delete $self->{files}->{$file};
205             }
206             else {
207 0         0 $self->_error("$file: no such member")
208             }
209             }
210 2         3 @{$self->{names}} = grep($self->{files}->{$_}, @{$self->{names}});
  2         7  
  2         10  
211              
212 2         3 return $nfiles_orig - scalar @{$self->{names}};
  2         9  
213             }
214              
215             sub list_files {
216 17     17 1 3067 my $self = shift;
217              
218 17 100       80 return wantarray ? @{$self->{names}} : $self->{names};
  8         52  
219             }
220              
221             sub add_files {
222 2     2 1 72 my $self = shift;
223 2 50       7 my $files = ref $_[0] ? shift : \@_;
224              
225 2         6 for my $path (@$files) {
226 6 50       280 if (open my $fd, $path) {
227 6 50       66 my @st = stat $fd or return $self->_error("$path: $!");
228 6         29 local $/ = undef;
229 6         12 binmode $fd;
230 6         137 my $content = <$fd>;
231 6         60 close $fd;
232              
233 6         89 my $filename = (File::Spec->splitpath($path))[2];
234              
235 6         25 $self->_add_data($filename, $content, @st[9,4,5,2,7]);
236             }
237             else {
238 0         0 $self->_error("$path: $!");
239             }
240             }
241 2         3 return scalar @{$self->{names}};
  2         10  
242             }
243              
244             sub add_data {
245 8     8 1 35 my $self = shift;
246 8         12 my $path = shift;
247 8         11 my $content = shift;
248 8   100     34 my $params = shift || {};
249              
250 8 50       19 return $self->_error("No filename given") unless $path;
251              
252 8         111 my $filename = (File::Spec->splitpath($path))[2];
253              
254 8 50 33     248 $self->_add_data($filename, $content,
      100        
      100        
      100        
255             $params->{date} || timelocal(localtime()),
256             $params->{uid} || 0,
257             $params->{gid} || 0,
258             $params->{mode} || 0100644) or return;
259              
260 8         43 return $self->{files}->{$filename}->{size};
261             }
262              
263             sub write {
264 7     7 1 4146 my $self = shift;
265 7         15 my $filename = shift;
266 7 50       12 my $opts = {(%{$self->{opts}}, %{shift || {}})};
  7         33  
  7         89  
267 7   100     71 my $type = $opts->{type} || $self->{type} || COMMON;
268              
269 7         21 my @body = ( ARMAG );
270              
271 7         13 my %gnuindex;
272 7         13 my @filenames = @{$self->{names}};
  7         22  
273 7 100       47 if ($type eq GNU) {
274             #
275             # construct extended filename index, if needed
276             #
277 3 50       19 if (my @longs = grep(length($_) > 15, @filenames)) {
278 3         6 my $ptr = 0;
279 3         7 for my $long (@longs) {
280 3         7 $gnuindex{$long} = $ptr;
281 3         10 $ptr += length($long) + 2;
282             }
283 3         24 push @body, pack('A16A32A10A2', '//', '', $ptr, ARFMAG),
284             join("/\n", @longs, '');
285 3 100       14 push @body, "\n" if $ptr % 2; # padding
286             }
287             }
288 7         28 for my $fn (@filenames) {
289 13         35 my $meta = $self->{files}->{$fn};
290 13         52 my $mode = sprintf('%o', $meta->{mode});
291 13         27 my $size = $meta->{size};
292 13         15 my $name;
293              
294 13 100       44 if ($type eq GNU) {
295 7 100 100     29 $fn = '' if defined $opts->{symbols} && $fn eq $opts->{symbols};
296 7         12 $name = $fn . '/';
297             }
298             else {
299 6         13 $name = $fn;
300             }
301 13 100 66     76 if (length($name) <= 16 || $type eq COMMON) {
    100          
    50          
302 9         77 push @body, pack('A16A12A6A6A8A10A2', $name,
303             @$meta{qw/date uid gid/}, $mode, $size, ARFMAG);
304             }
305             elsif ($type eq GNU) {
306 3         19 push @body, pack('A1A15A12A6A6A8A10A2', '/', $gnuindex{$fn},
307             @$meta{qw/date uid gid/}, $mode, $size, ARFMAG);
308             }
309             elsif ($type eq BSD) {
310 1         2 $size += length($name);
311 1         15 push @body, pack('A3A13A12A6A6A8A10A2', AR_EFMT1, length($name),
312             @$meta{qw/date uid gid/}, $mode, $size, ARFMAG),
313             $name;
314             }
315             else {
316 0         0 return $self->_error("$type: unexpected ar type");
317             }
318 13         25 push @body, $meta->{data};
319 13 100       54 push @body, "\n" if $size % 2; # padding
320             }
321 7 100       25 if ($filename) {
322 1         5 my $fh = $self->_get_handle($filename, '>');
323 1         23 print $fh @body;
324 1         56 close $fh;
325 1         2 my $len = 0;
326 1         7 $len += length($_) for @body;
327 1         7 return $len;
328             }
329             else {
330 6         45 return join '', @body;
331             }
332             }
333              
334             sub get_content {
335 20     20 1 18591 my $self = shift;
336 20         30 my ($filename) = @_;
337              
338 20 50       56 unless ($filename) {
339 0         0 $self->_error("get_content can't continue without a filename");
340 0         0 return;
341             }
342              
343 20 100       77 unless (exists($self->{files}->{$filename})) {
344 2         14 $self->_error(
345             "get_content failed because there is not a file named $filename");
346 2         8 return;
347             }
348              
349 18         87 return $self->{files}->{$filename};
350             }
351              
352             sub get_data {
353 3     3 1 5 my $self = shift;
354 3         5 my $filename = shift;
355              
356 3 50       11 return $self->_error("$filename: no such member")
357             unless exists $self->{files}->{$filename};
358 3         15 return $self->{files}->{$filename}->{data};
359             }
360              
361             sub get_handle {
362 3     3 1 1889 my $self = shift;
363 3         9 my $filename = shift;
364 3         3 my $fh;
365              
366 3 50       20 return $self->_error("$filename: no such member")
367             unless exists $self->{files}->{$filename};
368 3 50       8 if ($has_io_string) {
369 0         0 $fh = IO::String->new($self->{files}->{$filename}->{data});
370             }
371             else {
372 3         8 my $data = $self->{files}->{$filename}->{data};
373 3 50   1   61 open $fh, '<', \$data or return $self->_error("in-memory file: $!");
  1         10  
  1         1  
  1         8  
374             }
375 3         1645 return $fh;
376             }
377              
378             sub error {
379 0     0 1 0 my $self = shift;
380              
381 0 0       0 return shift() ? $self->{longmess} : $self->{error};
382             }
383              
384             #
385             # deprecated
386             #
387             sub DEBUG {
388 1     1 0 2 my $self = shift;
389 1         2 my $debug = shift;
390              
391 1 50 33     9 $self->{opts}->{warn} = 1 unless (defined($debug) and int($debug) == 0);
392             }
393              
394             sub _parse {
395 15     15   26 my $self = shift;
396 15         28 my $data = shift;
397              
398 15 50       114 unless (substr($data, 0, SARMAG, '') eq ARMAG) {
399 0         0 return $self->_error("Bad magic number - not an ar archive");
400             }
401 15         26 my $type;
402             my $names;
403 15         99 while ($data =~ /\S/) {
404 41         289 my ($name, $date, $uid, $gid, $mode, $size, $magic) =
405             unpack('A16A12A6A6A8A10a2', substr($data, 0, 60, ''));
406 41 50       124 unless ($magic eq "`\n") {
407 0         0 return $self->_error("Bad file header");
408             }
409 41 100       167 if ($name =~ m|^/|) {
    100          
410 8         11 $type = GNU;
411 8 100       21 if ($name eq '//') {
    100          
412 3         8 $names = substr($data, 0, $size, '');
413 3         9 substr($data, 0, $size % 2, '');
414 3         39 next;
415             }
416             elsif ($name eq '/') {
417 2         5 $name = $self->{opts}->{symbols};
418 2 100 66     9 unless (defined $name && $name) {
419 1         2 substr($data, 0, $size + $size % 2, '');
420 1         4 next;
421             }
422             }
423             else {
424 3         8 $name = substr($names, int(substr($name, 1)));
425 3         18 $name =~ s/\n.*//;
426 3         7 chop $name;
427             }
428             }
429             elsif ($name =~ m|^#1/|) {
430 1         3 $type = BSD;
431 1         4 $name = substr($data, 0, int(substr($name, 3)), '');
432 1         3 $size -= length($name);
433             }
434             else {
435 32 100       120 if ($name =~ m|/$|) {
436 3   50     9 $type ||= GNU; # only gnu has trailing slashes
437 3         9 chop $name;
438             }
439             }
440 37         66 $uid = int($uid);
441 37         46 $gid = int($gid);
442 37         59 $mode = oct($mode);
443 37         85 my $content = substr($data, 0, $size, '');
444 37         128 substr($data, 0, $size % 2, '');
445              
446 37         111 $self->_add_data($name, $content, $date, $uid, $gid, $mode, $size);
447             }
448 15   100     111 $self->{type} = $type || COMMON;
449 15         28 return scalar @{$self->{names}};
  15         66  
450             }
451              
452             sub _add_data {
453 51     51   534 my $self = shift;
454 51         65 my $filename = shift;
455 51   100     135 my $content = shift || '';
456 51         70 my $date = shift;
457 51         60 my $uid = shift;
458 51         71 my $gid = shift;
459 51         57 my $mode = shift;
460 51         68 my $size = shift;
461              
462 51 50       143 if (exists($self->{files}->{$filename})) {
463 0         0 return $self->_error("$filename: entry already exists");
464             }
465 51 50       583 $self->{files}->{$filename} = {
    50          
    50          
    50          
    100          
466             name => $filename,
467             date => defined $date ? $date : timelocal(localtime()),
468             uid => defined $uid ? $uid : 0,
469             gid => defined $gid ? $gid : 0,
470             mode => defined $mode ? $mode : 0100644,
471             size => defined $size ? $size : length($content),
472             data => $content,
473             };
474 51         81 push @{$self->{names}}, $filename;
  51         116  
475 51         220 return 1;
476             }
477              
478             sub _get_handle {
479 8     8   13 my $self = shift;
480 8         16 my $file = shift;
481 8   100     44 my $mode = shift || '<';
482              
483 8 100       25 if (ref $file) {
484 2 50 33     5 return $file if eval{*$file{IO}} or $file->isa('IO::Handle');
  2         40  
485 0         0 return $self->_error("Not a filehandle");
486             }
487             else {
488 6 50       280 open my $fh, $mode, $file or return $self->_error("$file: $!");
489 6         18 binmode $fh;
490 6         20 return $fh;
491             }
492             }
493              
494             sub _error {
495 2     2   3 my $self = shift;
496 2         3 my $msg = shift;
497              
498 2         4 $self->{error} = $msg;
499 2         296 $self->{longerror} = longmess($msg);
500 2 50       772 if ($self->{opts}->{warn} > 1) {
    50          
501 0         0 carp $self->{longerror};
502             }
503             elsif ($self->{opts}->{warn}) {
504 0         0 carp $self->{error};
505             }
506 2         5 return;
507             }
508              
509             1;
510              
511             __END__