File Coverage

tools/plha
Criterion Covered Total %
statement 206 337 61.1
branch 66 176 37.5
condition 17 57 29.8
subroutine 35 45 77.7
pod n/a
total 324 615 52.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2 14     14   72508 use strict;
  14         26  
  14         600  
3 14     14   61 use warnings;
  14         24  
  14         801  
4              
5 14     14   8643 use Data::Dumper;
  14         149634  
  14         1461  
6 14     14   8037 use Encode;
  14         257625  
  14         1412  
7 14     14   7163 use FindBin;
  14         19580  
  14         856  
8 14     14   100 use File::Spec;
  14         21  
  14         280  
9 14     14   82 use File::Basename;
  14         23  
  14         902  
10 14     14   110 use File::Path;
  14         31  
  14         1111  
11 14     14   10562 use Getopt::Long qw( GetOptionsFromArray );
  14         294338  
  14         100  
12 14     14   9632 use lib File::Spec->catfile($FindBin::Bin, '..', 'lib');
  14         11314  
  14         387  
13              
14 14     14   9336 use Archive::Lha::Decode;
  14         45  
  14         573  
15 14     14   6801 use Archive::Lha::Header;
  14         50  
  14         145  
16 14     14   98 use Archive::Lha::Header::Utils ();
  14         29  
  14         294  
17 14     14   28472 use Archive::Lha::Stream::File;
  14         39  
  14         501  
18 14     14   115 use Carp;
  14         29  
  14         1186  
19 14     14   95 use POSIX qw( strftime setlocale LC_TIME );
  14         29  
  14         114  
20 14     14   35232 use Time::Moment;
  14         24078  
  14         140291  
21              
22             # Charset options: -fc (from charset) and -tc (to charset)
23 14         2802900 my $opt_from_charset;
24             my $opt_to_charset;
25 14         0 my $opt_use_locale;
26              
27             # Parse --use-locale before anything else so setlocale runs at startup
28 14         166 Getopt::Long::GetOptionsFromArray(\@ARGV,
29             'use-locale' => \$opt_use_locale,
30             'from-charset|fc=s' => \$opt_from_charset,
31             'to-charset|tc=s' => \$opt_to_charset,
32             );
33 14 50       10910 setlocale(LC_TIME, 'C') unless $opt_use_locale;
34              
35             # Return display name for a header, respecting -fc/-tc options.
36             # Without options, pathname() auto-detects from the OS field.
37             sub _display_name {
38 224     224   330 my ($header) = @_;
39 224   100     936 return $header->pathname( $opt_from_charset, $opt_to_charset // 'UTF-8' );
40             }
41              
42             my $controller = +{
43             d => sub {
44 0 0   0   0 my $fname = shift or usage();
45 0         0 my $stream = open_archive($fname);
46 0         0 while ( defined( my $level = $stream->search_header ) ) {
47 0         0 my $header = Archive::Lha::Header->new(
48             level => $level,
49             stream => $stream
50             );
51 0         0 $stream->seek( $header->{next_header} );
52 0         0 print Dumper($header);
53             }
54              
55             },
56             l => sub {
57 1 50   1   3 my $fname = shift or usage();
58 1         3 my $stream = open_archive($fname);
59 1         6 while ( defined( my $level = $stream->search_header ) ) {
60 3         13 my $header = Archive::Lha::Header->new(
61             level => $level,
62             stream => $stream
63             );
64 3         12 $stream->seek( $header->{next_header} );
65 3         6 my $fullname = _display_name($header);
66 3 50       5 $fullname = '' if $fullname eq '.';
67 3   33     17 my $has_path = ($fullname =~ m{/} && !_is_directory($header));
68             # l shows filename only (no path), + prefix if file has a path component
69 3 50       9 my $name = $has_path ? (split m{/}, $fullname)[-1] : $fullname;
70 3 0 33     5 $name .= '/' if _is_directory($header) && $name ne '' && $name !~ m{/$};
      33        
71 3 50       9 my $prefix = $has_path ? '+' : ' ';
72 3         28 printf "%s%s\n", $prefix, $name;
73             }
74             },
75             v => sub {
76 5     5   10 my $contents = '';
77 5 50       31 my $fname = shift or usage();
78 5         19 my $stream = open_archive($fname);
79 5         25 my $totals = { original_size => 0, encoded_size => 0, count => 0 };
80 5         20 print "Original Packed Ratio Date Time Name\n";
81 5         12 print "-------- ------- ----- --------- -------- -------------\n";
82 5         42 while ( defined( my $level = $stream->search_header ) ) {
83 204         647 my $header = Archive::Lha::Header->new(
84             level => $level,
85             stream => $stream
86             );
87 204         532 $stream->seek( $header->{next_header} );
88              
89 204         328 $totals->{original_size} += $header->{original_size};
90 204         254 $totals->{encoded_size} += $header->{encoded_size};
91 204         223 $totals->{count} += 1;
92             printf "%8d %7d%5.1f%% %s %s %s\n",
93             $header->{original_size},
94             $header->{encoded_size},
95 204 100 66     987 (($header->{encoded_size} && $header->{original_size}) ? 100 * ($header->{original_size} - $header->{encoded_size}) / $header->{original_size} : 0),
96             _header_date($header),
97             _header_time($header),
98             _display_name($header);
99 204 50       1357 printf ": %s\n", $header->{comment} if $header->{comment};
100             }
101 5         13 print "-------- ------- ----- --------- --------\n";
102             printf "%8d %7d%5.1f%% %s %s %s\n",
103             $totals->{original_size},
104             $totals->{encoded_size},
105             (($totals->{encoded_size} && $totals->{original_size}) ? 100 * ($totals->{original_size} - $totals->{encoded_size}) / $totals->{original_size} : 0),
106             strftime("%d-%b-%y", localtime((stat($fname))[9])),
107             strftime("%T", localtime((stat($fname))[9])),
108 5 100 66     708 (sprintf(" %d files", $totals->{count}));
109             },
110             vv => sub {
111 1 50   1   5 my $fname = shift or usage();
112 1         5 my $stream = open_archive($fname);
113 1         6 my $totals = { original_size => 0, encoded_size => 0, count => 0 };
114 1         5 print "Original Packed Ratio Date Time Atts Method CRC L OS\n";
115 1         3 print "-------- ------- ----- --------- -------- -------- ------ ---- -----\n";
116 1         17 while ( defined( my $level = $stream->search_header ) ) {
117 3         42 my $header = Archive::Lha::Header->new(
118             level => $level,
119             stream => $stream
120             );
121 3         24 $stream->seek( $header->{next_header} );
122              
123 3         9 $totals->{original_size} += $header->{original_size};
124 3         7 $totals->{encoded_size} += $header->{encoded_size};
125 3         7 $totals->{count} += 1;
126              
127 3         8 my $name = _display_name($header);
128 3   50     10 my $os_char = uc($header->{os}[0] // '?');
129 3 50       24 my $hdr_level = ref($header) =~ /Level(\d)/ ? $1 : '?';
130              
131 3         14 printf "%s\n", $name;
132             printf "%8d %7d%5.1f%% %s %s ----rwed -%s- %04X %s %s\n",
133             $header->{original_size},
134             $header->{encoded_size},
135             (($header->{encoded_size} && $header->{original_size}) ? 100 * ($header->{original_size} - $header->{encoded_size}) / $header->{original_size} : 0),
136             _header_date($header),
137             _header_time($header),
138             $header->{method},
139             $header->{crc16},
140 3 50 33     76 $hdr_level,
141             $os_char;
142 3 50       46 printf ": %s\n", $header->{comment} if $header->{comment};
143             }
144 1         3 print "-------- ------- ----- --------- --------\n";
145             printf "%8d %7d%5.1f%% %s %s %s\n",
146             $totals->{original_size},
147             $totals->{encoded_size},
148             (($totals->{encoded_size} && $totals->{original_size}) ? 100 * ($totals->{original_size} - $totals->{encoded_size}) / $totals->{original_size} : 0),
149             strftime("%d-%b-%y", localtime((stat($fname))[9])),
150             strftime("%T", localtime((stat($fname))[9])),
151 1 50 33     135 (sprintf(" %d files", $totals->{count}));
152             },
153             t => sub {
154 0 0   0   0 my $fname = shift or usage();
155 0         0 printf "Testing integrity of archive '%s':\n", $fname;
156 0         0 my $stream = open_archive($fname);
157 0         0 my $totals = { original_size => 0, encoded_size => 0, count => 0 };
158 0         0 while ( defined( my $level = $stream->search_header ) ) {
159 0         0 my $header = Archive::Lha::Header->new(
160             level => $level,
161             stream => $stream
162             );
163 0         0 $stream->seek( $header->data_top );
164 0         0 _decode_entry($header, $stream);
165 0         0 $stream->seek( $header->{next_header} );
166              
167 0         0 $totals->{original_size} += $header->{original_size};
168 0         0 $totals->{encoded_size} += $header->{encoded_size};
169 0         0 $totals->{count} += 1;
170 0         0 printf " Testing: (%8d/%8d) %s\n", $header->{original_size}, $header->{original_size}, _display_name($header);
171             }
172 0         0 my $error = undef;
173 0 0       0 if ($totals->{count}) {
174 0 0       0 if (!$error) {
175 0         0 printf "%d files tested, all files OK\n", $totals->{count};
176             }
177             } else {
178 0         0 $error = 1;
179 0         0 printf "No files tested.\n";
180             }
181 0 0       0 if ($error) {
182 0         0 printf "\nOperation not entirely successful\n\n";
183             } else {
184 0         0 printf "\nOperation succesful\n\n";
185             }
186             },
187             x => sub {
188 0 0   0   0 my $fname = shift or usage();
189 0         0 my %target;
190 0 0       0 if (@_) {
191 0         0 %target = map { $_ => 1 } @_;
  0         0  
192             }
193 0         0 my $stream = open_archive($fname);
194 0         0 while ( defined( my $level = $stream->search_header ) ) {
195 0         0 my $header = Archive::Lha::Header->new(
196             level => $level,
197             stream => $stream
198             );
199 0 0 0     0 if ( %target and !$target{$header->pathname} ) {
200 0         0 $stream->seek( $header->next_header );
201 0         0 next;
202             }
203 0         0 $stream->seek( $header->data_top );
204              
205 0 0       0 if (_is_directory($header)) {
206 0 0       0 mkpath $header->pathname unless -d $header->pathname;
207 0         0 $stream->seek( $header->{next_header} );
208 0         0 next;
209             }
210              
211 0         0 my ($decoded, $crc) = _decode_entry($header, $stream);
212 0         0 $stream->seek( $header->{next_header} );
213 0 0       0 die "crc mismatch" if $crc != $header->crc16;
214              
215 0         0 write_all($header->pathname, $decoded);
216             }
217             },
218 14         375 };
219              
220 14 100       605 my $PROGNAME = $ENV{PLHASA} ? 'plhasa' : basename($0);
221              
222 14         57 &main;exit;
  13         0  
223              
224             sub main {
225 14 100   14   50 if ($PROGNAME eq 'plhasa') {
226 6         21 _main_lhasa();
227             } else {
228 8         29 _main_plha();
229             }
230             }
231              
232             sub _main_plha {
233 8     8   45 GetOptionsFromArray(\@ARGV,
234             'from-charset|fc=s' => \$opt_from_charset,
235             'to-charset|tc=s' => \$opt_to_charset,
236             'use-locale' => \$opt_use_locale,
237             );
238 8 50       2205 my $cmd = shift @ARGV or usage();
239 8 50       32 my $file = shift @ARGV or usage();
240 8         44 check_magic($file);
241 8 100       33 if ( !exists $controller->{$cmd} ) {
242 1         5 usage("Unknown command: $cmd");
243             }
244 7         29 $controller->{$cmd}->($file, @ARGV);
245             }
246              
247             # lhasa-compatible argument parsing:
248             # [-]{lvtxep[q{num}][finv]}[w=] archive_file [file...]
249             sub _main_lhasa {
250 6 50   6   24 my $arg = shift @ARGV or usage_lhasa();
251 6         19 $arg =~ s/^-//; # strip optional leading dash
252              
253             # extract command letter (first char)
254 6 50       38 my ($cmd_char) = $arg =~ /^([lvtxep])/i or usage_lhasa();
255 6         18 my $flags = substr($arg, 1); # everything after command char
256              
257             # parse options from flags string
258 6         46 my %opts = (quiet => 0, verbose => 0, force => 0, ignore_path => 0, dry_run => 0, extract_dir => undef);
259 6         23 while (length $flags) {
260 0 0       0 if ($flags =~ s/^q(\d*)//) {
    0          
    0          
    0          
    0          
    0          
261 0 0       0 $opts{quiet} = length($1) ? int($1) : 1;
262             } elsif ($flags =~ s/^w=([^\s]+)//) {
263 0         0 $opts{extract_dir} = $1;
264             } elsif ($flags =~ s/^f//) {
265 0         0 $opts{force} = 1;
266             } elsif ($flags =~ s/^i//) {
267 0         0 $opts{ignore_path} = 1;
268             } elsif ($flags =~ s/^n//) {
269 0         0 $opts{dry_run} = 1;
270             } elsif ($flags =~ s/^v//) {
271 0         0 $opts{verbose} = 1;
272             } else {
273 0         0 $flags = substr($flags, 1); # skip unknown flag
274             }
275             }
276              
277             # also allow w= as a separate argument
278 6 50 33     50 if (@ARGV && $ARGV[0] =~ /^w=(.+)/) {
279 0         0 $opts{extract_dir} = $1;
280 0         0 shift @ARGV;
281             }
282              
283 6 50       22 my $file = shift @ARGV or usage_lhasa();
284 6         30 check_magic($file);
285              
286 6         19 my $cmd = lc $cmd_char;
287 6 50       30 $cmd = 'x' if $cmd eq 'e';
288              
289 6 50       40 if ($cmd eq 'p') {
    50          
    100          
    50          
    0          
290 0         0 _print_to_stdout($file, \%opts, @ARGV);
291             } elsif ($cmd eq 'x') {
292 0         0 _extract_lhasa($file, \%opts, @ARGV);
293             } elsif ($cmd eq 'l') {
294 2         9 _list_lhasa($file, 'l');
295             } elsif ($cmd eq 'v') {
296 4         19 _list_lhasa($file, 'lv');
297             } elsif ($cmd eq 't') {
298 0         0 $controller->{t}->($file);
299             } else {
300 0         0 usage_lhasa();
301             }
302             }
303              
304             sub _print_to_stdout {
305 0     0   0 my ($fname, $opts, @targets) = @_;
306 0         0 my %target = map { $_ => 1 } @targets;
  0         0  
307 0         0 my $stream = open_archive($fname);
308 0         0 while ( defined( my $level = $stream->search_header ) ) {
309 0         0 my $header = Archive::Lha::Header->new( level => $level, stream => $stream );
310 0 0 0     0 if (%target && !$target{$header->pathname}) {
311 0         0 $stream->seek( $header->{next_header} );
312 0         0 next;
313             }
314 0 0       0 next if _is_directory($header);
315 0         0 $stream->seek( $header->data_top );
316 0         0 my ($decoded) = _decode_entry($header, $stream);
317 0         0 $stream->seek( $header->{next_header} );
318 0         0 print $decoded;
319             }
320             }
321              
322             sub _extract_lhasa {
323 0     0   0 my ($fname, $opts, @targets) = @_;
324 0         0 my %target = map { $_ => 1 } @targets;
  0         0  
325 0         0 my $stream = open_archive($fname);
326 0         0 while ( defined( my $level = $stream->search_header ) ) {
327 0         0 my $header = Archive::Lha::Header->new( level => $level, stream => $stream );
328 0         0 my $pathname = $header->pathname;
329 0 0       0 $pathname =~ s{.*/}{} if $opts->{ignore_path};
330 0 0 0     0 if (%target && !$target{$pathname}) {
331 0         0 $stream->seek( $header->{next_header} );
332 0         0 next;
333             }
334             $pathname = File::Spec->catfile($opts->{extract_dir}, $pathname)
335 0 0       0 if $opts->{extract_dir};
336              
337 0 0       0 if (_is_directory($header)) {
338 0 0 0     0 mkpath $pathname unless -d $pathname || $opts->{dry_run};
339 0         0 $stream->seek( $header->{next_header} );
340 0         0 next;
341             }
342              
343 0         0 $stream->seek( $header->data_top );
344 0         0 my ($decoded, $crc) = _decode_entry($header, $stream);
345 0         0 $stream->seek( $header->{next_header} );
346 0 0       0 die "crc mismatch for " . $header->pathname if $crc != $header->crc16;
347              
348 0 0       0 unless ($opts->{dry_run}) {
349 0 0 0     0 if (-e $pathname && !$opts->{force}) {
350 0         0 print STDERR "$pathname already exists, skipping (use -f to force)\n";
351 0         0 next;
352             }
353 0         0 write_all($pathname, $decoded);
354             }
355 0 0       0 printf " %s\n", $pathname if $opts->{verbose};
356             }
357             }
358              
359             sub usage_lhasa {
360 0     0   0 die "plhasa -- Perl LHA tool (lhasa-compatible)\n" .
361             "usage: plhasa [-]{lvtxep[q{num}][finv]}[w=] archive_file [file...]\n" .
362             "commands: options:\n" .
363             " l List (terse) f Force overwrite (no prompt)\n" .
364             " v Verbose list i Ignore directory path\n" .
365             " t Test file CRC in archive n Perform dry run\n" .
366             " x,e Extract from archive q{num} Quiet mode\n" .
367             " p Print to stdout from archive v Verbose\n" .
368             " w= Specify extract directory\n";
369             }
370              
371             sub usage {
372 1     1   1 my ($msg) = @_;
373 1         6 my $text = "Usage: $0 [options] (l|v|vv|x|t|d) archive (files)\n" .
374             " l - list contents (LhA terse format, filename only)\n" .
375             " v - list archive verbose (LhA v format)\n" .
376             " vv - list archive full (LhA vv format)\n" .
377             " x - extract archive\n" .
378             " t - test file\n" .
379             " d - dump each header\n" .
380             " -fc, --from-charset source encoding for filenames (default: auto-detect)\n" .
381             " -tc, --to-charset output encoding for filenames (default: UTF-8)\n" .
382             " --use-locale use system locale for month names (default: English)\n";
383 1 50       2 if ($msg) {
384 1         0 die "$msg\n$text";
385             }
386 0         0 die $text;
387             }
388              
389             sub _header_date {
390 207     207   276 my ($h) = @_;
391             return $h->{timestamp_is_unix}
392             ? strftime("%d-%b-%y", localtime($h->{timestamp}))
393 207 100       1089 : strftime("%d-%b-%y", Archive::Lha::Header::Utils::dostime_fields($h->{timestamp}));
394             }
395              
396             sub _header_time {
397 207     207   328 my ($h) = @_;
398             return $h->{timestamp_is_unix}
399             ? strftime("%T", localtime($h->{timestamp}))
400 207 100       689 : strftime("%T", Archive::Lha::Header::Utils::dostime_fields($h->{timestamp}));
401             }
402              
403             # ls-style date from Unix epoch (stat mtime etc)
404             # Note: avoid %e (space-padded day) — not supported on Windows MSVC runtime.
405             # Use %d and strip the leading zero manually instead.
406             sub _ls_stamp {
407 20     20   39 my ($epoch) = @_;
408 20         36 my $six_months = 6 * 30 * 86400;
409 20         467 my @t = localtime($epoch);
410 20         94 my $day = sprintf "%2d", $t[3]; # space-pad day
411 20 100       92 if (abs(time - $epoch) < $six_months) {
412 2         55 return strftime("%b", @t) . " $day " . strftime("%H:%M", @t);
413             }
414 18         563 return strftime("%b", @t) . " $day " . strftime("%Y", @t);
415             }
416              
417             # ls-style date from a header (handles both DOS and Unix timestamps)
418             sub _ls_stamp_header {
419 14     14   44 my ($header) = @_;
420             my $epoch = $header->{timestamp_is_unix}
421             ? $header->{timestamp}
422 14 100       60 : Archive::Lha::Header::Utils::_dostime2utime($header->{timestamp});
423 14         41 return _ls_stamp($epoch);
424             }
425              
426             # Lhasa-compatible listing (l = terse, lv = verbose with method+crc)
427             sub _list_lhasa {
428 6     6   18 my ($fname, $mode) = @_;
429 6         22 my $stream = open_archive($fname);
430 6         61 my $totals = { original_size => 0, encoded_size => 0, count => 0 };
431              
432 6 100       26 if ($mode eq 'lv') {
433 4         23 printf " PERMSSN UID GID PACKED SIZE RATIO METHOD CRC STAMP NAME\n";
434 4         20 printf "---------- ----------- ------- ------- ------ ---------- ------------ -------------\n";
435             } else {
436 2         10 printf " PERMSSN UID GID SIZE RATIO STAMP NAME\n";
437 2         6 printf "---------- ----------- ------- ------ ------------ --------------------\n";
438             }
439              
440 6         99 while ( defined( my $level = $stream->search_header ) ) {
441 14         114 my $header = Archive::Lha::Header->new(
442             level => $level,
443             stream => $stream
444             );
445             # Skip to next header - no need to decode for listing
446 14         117 $stream->seek( $header->{next_header} );
447              
448 14         38 $totals->{original_size} += $header->{original_size};
449 14         38 $totals->{encoded_size} += $header->{encoded_size};
450 14         29 $totals->{count} += 1;
451              
452 14         45 my $stamp = _ls_stamp_header($header);
453 14         58 my $name = _display_name($header);
454 14 50       40 $name = '' if $name eq '.'; # empty root directory
455 14 0 33     51 $name .= '/' if _is_directory($header) && $name ne '' && $name !~ m{/$};
      33        
456 14 100 50     80 $name = _fix_msdos_allcaps($name) if ($header->{os}[0] // '') eq 'M';
457 14         41 my $prefix = _lhasa_prefix($header);
458             my $ratio_str = _is_directory($header) ? '******'
459             : sprintf("%5.1f%%", $header->{original_size}
460 14 50       56 ? unpack('f', pack('f', 100 * $header->{encoded_size} / $header->{original_size})) : 100);
    50          
461              
462 14 100       47 if ($mode eq 'lv') {
463             printf "%s%7d %7d %s -%s- %04x %s %s\n",
464             $prefix,
465             $header->{encoded_size},
466             $header->{original_size},
467             $ratio_str,
468             $header->{method},
469             $header->{crc16},
470 10         188 $stamp,
471             $name;
472             } else {
473             printf "%s%7d %s %s %s\n",
474             $prefix,
475             $header->{original_size},
476 4         97 $ratio_str,
477             $stamp,
478             $name;
479             }
480             }
481              
482 6 100       24 if ($mode eq 'lv') {
483 4         17 printf "---------- ----------- ------- ------- ------ ---------- ------------ -------------\n";
484             } else {
485 2         7 printf "---------- ----------- ------- ------ ------------ --------------------\n";
486             }
487              
488             my $ratio = $totals->{original_size}
489 6 50       61 ? unpack('f', pack('f', 100 * $totals->{encoded_size} / $totals->{original_size})) : 100;
490 6         168 my $stamp = _ls_stamp((stat($fname))[9]);
491              
492             # PERMSSN (10) + sep (1) + UID/GID (11) + sep (1) = 23 chars for prefix
493             # " Total " (PERMSSN 10) + " " (sep) + "%5d files" (UID/GID 11) + " " (sep) = 23
494 6 100       39 my $file_str = $totals->{count} == 1 ? 'file ' : 'files';
495 6         39 my $prefix = " Total " . sprintf(" %5d %s ", $totals->{count}, $file_str);
496              
497 6 100       37 if ($mode eq 'lv') {
498             printf "%s%7d %7d %5.1f%% %s\n",
499             $prefix,
500             $totals->{encoded_size},
501             $totals->{original_size},
502 4         180 $ratio,
503             $stamp;
504             } else {
505             printf "%s%7d %5.1f%% %s\n",
506             $prefix,
507             $totals->{original_size},
508 2         78 $ratio,
509             $stamp;
510             }
511             }
512              
513 34     34   376 sub _is_directory { $_[0]->{method} eq 'lhd' }
514              
515             # MS-DOS archives store filenames in all-caps. Lhasa detects per-file
516             # all-caps paths and converts to lowercase. Match that behavior.
517             sub _fix_msdos_allcaps {
518 12     12   29 my ($name) = @_;
519 12 50       64 return $name if $name =~ /[a-z]/; # has lowercase = not all-caps
520 0         0 return lc $name;
521             }
522              
523             # Format permission/owner prefix like lhasa does
524             sub _lhasa_prefix {
525 14     14   30 my ($header) = @_;
526 14 50       54 if (defined $header->{unix_perm}) {
527 0         0 my $perm = $header->{unix_perm};
528 0 0       0 my $type = _is_directory($header) ? 'd' : '-';
529 0         0 my $str = $type;
530 0         0 for my $shift (6, 3, 0) {
531 0         0 my $bits = ($perm >> $shift) & 7;
532 0 0       0 $str .= ($bits & 4) ? 'r' : '-';
533 0 0       0 $str .= ($bits & 2) ? 'w' : '-';
534 0 0       0 $str .= ($bits & 1) ? 'x' : '-';
535             }
536 0   0     0 my $uid = $header->{unix_uid} // 0;
537 0   0     0 my $gid = $header->{unix_gid} // 0;
538             # PERMSSN(10) + sep(1) + UID/GID(%5d/%-5d = 11) + sep(1) = 23
539 0         0 return sprintf "%s %5d/%-5d ", $str, $uid, $gid;
540             }
541 14   50     93 return sprintf "%-23s", '[' . ($header->{os}[1] // 'generic') . ']';
542             }
543              
544             sub _decode_entry {
545 0     0   0 my ($header, $stream) = @_;
546 0 0       0 return ('', 0) if _is_directory($header);
547 0         0 my $decoded = '';
548             my $decoder = Archive::Lha::Decode->new(
549             header => $header,
550 0     0   0 read => sub { $stream->read(@_) },
551 0     0   0 write => sub { $decoded .= join '', @_ },
552 0         0 );
553 0         0 my $crc = $decoder->decode;
554 0         0 return ($decoded, $crc);
555             }
556              
557             sub open_archive {
558 13     13   53 my $fname = shift;
559 13 50       48 die "fname missing" unless $fname;
560 13         214 Archive::Lha::Stream::File->new(file => $fname);
561             }
562              
563             sub write_all {
564 0     0   0 my ($fname, $data) = @_;
565 0         0 my $dir = dirname($fname);
566 0 0       0 mkpath $dir unless -d $dir;
567 0 0       0 open my $fh, '>:raw', $fname or die $!;
568 0         0 binmode $fh;
569 0         0 print $fh $data;
570 0         0 close $fh;
571             }
572              
573             sub check_magic {
574 14     14   138 my $fname = shift;
575 14 50       1294 open my $fh, '<:raw', $fname or die "Cannot open $fname: $!";
576 14         52 binmode $fh;
577 14         67 my $magic;
578 14         258 my $chars = read($fh, $magic, 5);
579 14         242 my ($signature) = unpack("x2a3", $magic);
580 14 50       154 die 'Does not look like an LHa file' unless $signature eq "-lh";
581              
582             # Check for truncation: last byte of a well-formed LHA archive is 0x00
583 14         152 seek $fh, -1, 2;
584 14         65 my $last_byte;
585 14         88 read $fh, $last_byte, 1;
586 14 100       59 if ( ord($last_byte) != 0x00 ) {
587 1         23 warn "WARNING: Archive may be truncated or corrupt (last byte is not 0x00)\n";
588             }
589 14         210 close $fh;
590             }
591              
592             __END__