Subversion Repositories DevTools

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
6954 dpurdie 1
package Archive::Zip::Archive;
2
 
3
# Represents a generic ZIP archive
4
 
5
use strict;
6
use File::Path;
7
use File::Find ();
8
use File::Spec ();
9
use File::Copy ();
10
use File::Basename;
11
use Cwd;
12
use Encode qw(encode_utf8 decode_utf8);
13
 
14
use vars qw( $VERSION @ISA );
15
 
16
BEGIN {
17
    $VERSION = '1.57';
18
    @ISA     = qw( Archive::Zip );
19
}
20
 
21
use Archive::Zip qw(
22
  :CONSTANTS
23
  :ERROR_CODES
24
  :PKZIP_CONSTANTS
25
  :UTILITY_METHODS
26
);
27
 
28
our $UNICODE;
29
 
30
# Note that this returns undef on read errors, else new zip object.
31
 
32
sub new {
33
    my $class = shift;
34
    my $self  = bless(
35
        {
36
            'diskNumber'                            => 0,
37
            'diskNumberWithStartOfCentralDirectory' => 0,
38
            'numberOfCentralDirectoriesOnThisDisk' =>
39
              0,    # should be # of members
40
            'numberOfCentralDirectories' => 0,    # should be # of members
41
            'centralDirectorySize'       => 0,    # must re-compute on write
42
            'centralDirectoryOffsetWRTStartingDiskNumber' =>
43
              0,                                  # must re-compute
44
            'writeEOCDOffset'             => 0,
45
            'writeCentralDirectoryOffset' => 0,
46
            'zipfileComment'              => '',
47
            'eocdOffset'                  => 0,
48
            'fileName'                    => ''
49
        },
50
        $class
51
    );
52
    $self->{'members'} = [];
53
    my $fileName = (ref($_[0]) eq 'HASH') ? shift->{filename} : shift;
54
    if ($fileName) {
55
        my $status = $self->read($fileName);
56
        return $status == AZ_OK ? $self : undef;
57
    }
58
    return $self;
59
}
60
 
61
sub storeSymbolicLink {
62
    my $self = shift;
63
    $self->{'storeSymbolicLink'} = shift;
64
}
65
 
66
sub members {
67
    @{shift->{'members'}};
68
}
69
 
70
sub numberOfMembers {
71
    scalar(shift->members());
72
}
73
 
74
sub memberNames {
75
    my $self = shift;
76
    return map { $_->fileName() } $self->members();
77
}
78
 
79
# return ref to member with given name or undef
80
sub memberNamed {
81
    my $self = shift;
82
    my $fileName = (ref($_[0]) eq 'HASH') ? shift->{zipName} : shift;
83
    foreach my $member ($self->members()) {
84
        return $member if $member->fileName() eq $fileName;
85
    }
86
    return undef;
87
}
88
 
89
sub membersMatching {
90
    my $self = shift;
91
    my $pattern = (ref($_[0]) eq 'HASH') ? shift->{regex} : shift;
92
    return grep { $_->fileName() =~ /$pattern/ } $self->members();
93
}
94
 
95
sub diskNumber {
96
    shift->{'diskNumber'};
97
}
98
 
99
sub diskNumberWithStartOfCentralDirectory {
100
    shift->{'diskNumberWithStartOfCentralDirectory'};
101
}
102
 
103
sub numberOfCentralDirectoriesOnThisDisk {
104
    shift->{'numberOfCentralDirectoriesOnThisDisk'};
105
}
106
 
107
sub numberOfCentralDirectories {
108
    shift->{'numberOfCentralDirectories'};
109
}
110
 
111
sub centralDirectorySize {
112
    shift->{'centralDirectorySize'};
113
}
114
 
115
sub centralDirectoryOffsetWRTStartingDiskNumber {
116
    shift->{'centralDirectoryOffsetWRTStartingDiskNumber'};
117
}
118
 
119
sub zipfileComment {
120
    my $self    = shift;
121
    my $comment = $self->{'zipfileComment'};
122
    if (@_) {
123
        my $new_comment = (ref($_[0]) eq 'HASH') ? shift->{comment} : shift;
124
        $self->{'zipfileComment'} = pack('C0a*', $new_comment);  # avoid Unicode
125
    }
126
    return $comment;
127
}
128
 
129
sub eocdOffset {
130
    shift->{'eocdOffset'};
131
}
132
 
133
# Return the name of the file last read.
134
sub fileName {
135
    shift->{'fileName'};
136
}
137
 
138
sub removeMember {
139
    my $self = shift;
140
    my $member = (ref($_[0]) eq 'HASH') ? shift->{memberOrZipName} : shift;
141
    $member = $self->memberNamed($member) unless ref($member);
142
    return undef unless $member;
143
    my @newMembers = grep { $_ != $member } $self->members();
144
    $self->{'members'} = \@newMembers;
145
    return $member;
146
}
147
 
148
sub replaceMember {
149
    my $self = shift;
150
 
151
    my ($oldMember, $newMember);
152
    if (ref($_[0]) eq 'HASH') {
153
        $oldMember = $_[0]->{memberOrZipName};
154
        $newMember = $_[0]->{newMember};
155
    } else {
156
        ($oldMember, $newMember) = @_;
157
    }
158
 
159
    $oldMember = $self->memberNamed($oldMember) unless ref($oldMember);
160
    return undef unless $oldMember;
161
    return undef unless $newMember;
162
    my @newMembers =
163
      map { ($_ == $oldMember) ? $newMember : $_ } $self->members();
164
    $self->{'members'} = \@newMembers;
165
    return $oldMember;
166
}
167
 
168
sub extractMember {
169
    my $self = shift;
170
 
171
    my ($member, $name);
172
    if (ref($_[0]) eq 'HASH') {
173
        $member = $_[0]->{memberOrZipName};
174
        $name   = $_[0]->{name};
175
    } else {
176
        ($member, $name) = @_;
177
    }
178
 
179
    $member = $self->memberNamed($member) unless ref($member);
180
    return _error('member not found') unless $member;
181
    my $originalSize = $member->compressedSize();
182
    my ($volumeName, $dirName, $fileName);
183
    if (defined($name)) {
184
        ($volumeName, $dirName, $fileName) = File::Spec->splitpath($name);
185
        $dirName = File::Spec->catpath($volumeName, $dirName, '');
186
    } else {
187
        $name = $member->fileName();
188
        ($dirName = $name) =~ s{[^/]*$}{};
189
        $dirName = Archive::Zip::_asLocalName($dirName);
190
        $name    = Archive::Zip::_asLocalName($name);
191
    }
192
    if ($dirName && !-d $dirName) {
193
        mkpath($dirName);
194
        return _ioError("can't create dir $dirName") if (!-d $dirName);
195
    }
196
    my $rc = $member->extractToFileNamed($name, @_);
197
 
198
    # TODO refactor this fix into extractToFileNamed()
199
    $member->{'compressedSize'} = $originalSize;
200
    return $rc;
201
}
202
 
203
sub extractMemberWithoutPaths {
204
    my $self = shift;
205
 
206
    my ($member, $name);
207
    if (ref($_[0]) eq 'HASH') {
208
        $member = $_[0]->{memberOrZipName};
209
        $name   = $_[0]->{name};
210
    } else {
211
        ($member, $name) = @_;
212
    }
213
 
214
    $member = $self->memberNamed($member) unless ref($member);
215
    return _error('member not found') unless $member;
216
    my $originalSize = $member->compressedSize();
217
    return AZ_OK if $member->isDirectory();
218
    unless ($name) {
219
        $name = $member->fileName();
220
        $name =~ s{.*/}{};    # strip off directories, if any
221
        $name = Archive::Zip::_asLocalName($name);
222
    }
223
    my $rc = $member->extractToFileNamed($name, @_);
224
    $member->{'compressedSize'} = $originalSize;
225
    return $rc;
226
}
227
 
228
sub addMember {
229
    my $self = shift;
230
    my $newMember = (ref($_[0]) eq 'HASH') ? shift->{member} : shift;
231
    push(@{$self->{'members'}}, $newMember) if $newMember;
232
    if($newMember && ($newMember->{bitFlag} & 0x800) 
233
                  && !utf8::is_utf8($newMember->{fileName})){
234
        $newMember->{fileName} = Encode::decode_utf8( $newMember->{fileName} );
235
    }
236
    return $newMember;
237
}
238
 
239
sub addFile {
240
    my $self = shift;
241
 
242
    my ($fileName, $newName, $compressionLevel);
243
    if (ref($_[0]) eq 'HASH') {
244
        $fileName         = $_[0]->{filename};
245
        $newName          = $_[0]->{zipName};
246
        $compressionLevel = $_[0]->{compressionLevel};
247
    } else {
248
        ($fileName, $newName, $compressionLevel) = @_;
249
    }
250
 
251
    if ($^O eq 'MSWin32' && $Archive::Zip::UNICODE) {
252
        $fileName = Win32::GetANSIPathName($fileName);
253
    }
254
 
255
    my $newMember = Archive::Zip::Member->newFromFile($fileName, $newName);
256
    $newMember->desiredCompressionLevel($compressionLevel);
257
    if ($self->{'storeSymbolicLink'} && -l $fileName) {
258
        my $newMember =
259
          Archive::Zip::Member->newFromString(readlink $fileName, $newName);
260
 
261
  # For symbolic links, External File Attribute is set to 0xA1FF0000 by Info-ZIP
262
        $newMember->{'externalFileAttributes'} = 0xA1FF0000;
263
        $self->addMember($newMember);
264
    } else {
265
        $self->addMember($newMember);
266
    }
267
 
268
    return $newMember;
269
}
270
 
271
sub addString {
272
    my $self = shift;
273
 
274
    my ($stringOrStringRef, $name, $compressionLevel);
275
    if (ref($_[0]) eq 'HASH') {
276
        $stringOrStringRef = $_[0]->{string};
277
        $name              = $_[0]->{zipName};
278
        $compressionLevel  = $_[0]->{compressionLevel};
279
    } else {
280
        ($stringOrStringRef, $name, $compressionLevel) = @_;
281
    }
282
 
283
    my $newMember =
284
      Archive::Zip::Member->newFromString($stringOrStringRef, $name);
285
    $newMember->desiredCompressionLevel($compressionLevel);
286
    return $self->addMember($newMember);
287
}
288
 
289
sub addDirectory {
290
    my $self = shift;
291
 
292
    my ($name, $newName);
293
    if (ref($_[0]) eq 'HASH') {
294
        $name    = $_[0]->{directoryName};
295
        $newName = $_[0]->{zipName};
296
    } else {
297
        ($name, $newName) = @_;
298
    }
299
 
300
    if ($^O eq 'MSWin32' && $Archive::Zip::UNICODE) {
301
        $name = Win32::GetANSIPathName($name);
302
    }
303
 
304
    my $newMember = Archive::Zip::Member->newDirectoryNamed($name, $newName);
305
    if ($self->{'storeSymbolicLink'} && -l $name) {
306
        my $link = readlink $name;
307
        ($newName =~ s{/$}{}) if $newName;    # Strip trailing /
308
        my $newMember = Archive::Zip::Member->newFromString($link, $newName);
309
 
310
  # For symbolic links, External File Attribute is set to 0xA1FF0000 by Info-ZIP
311
        $newMember->{'externalFileAttributes'} = 0xA1FF0000;
312
        $self->addMember($newMember);
313
    } else {
314
        $self->addMember($newMember);
315
    }
316
 
317
    return $newMember;
318
}
319
 
320
# add either a file or a directory.
321
 
322
sub addFileOrDirectory {
323
    my $self = shift;
324
 
325
    my ($name, $newName, $compressionLevel);
326
    if (ref($_[0]) eq 'HASH') {
327
        $name             = $_[0]->{name};
328
        $newName          = $_[0]->{zipName};
329
        $compressionLevel = $_[0]->{compressionLevel};
330
    } else {
331
        ($name, $newName, $compressionLevel) = @_;
332
    }
333
 
334
    if ($^O eq 'MSWin32' && $Archive::Zip::UNICODE) {
335
        $name = Win32::GetANSIPathName($name);
336
    }
337
 
338
    $name =~ s{/$}{};
339
    if ($newName) {
340
        $newName =~ s{/$}{};
341
    } else {
342
        $newName = $name;
343
    }
344
    if (-f $name) {
345
        return $self->addFile($name, $newName, $compressionLevel);
346
    } elsif (-d $name) {
347
        return $self->addDirectory($name, $newName);
348
    } else {
349
        return _error("$name is neither a file nor a directory");
350
    }
351
}
352
 
353
sub contents {
354
    my $self = shift;
355
 
356
    my ($member, $newContents);
357
    if (ref($_[0]) eq 'HASH') {
358
        $member      = $_[0]->{memberOrZipName};
359
        $newContents = $_[0]->{contents};
360
    } else {
361
        ($member, $newContents) = @_;
362
    }
363
 
364
    return _error('No member name given') unless $member;
365
    $member = $self->memberNamed($member) unless ref($member);
366
    return undef unless $member;
367
    return $member->contents($newContents);
368
}
369
 
370
sub writeToFileNamed {
371
    my $self = shift;
372
    my $fileName =
373
      (ref($_[0]) eq 'HASH') ? shift->{filename} : shift;    # local FS format
374
    foreach my $member ($self->members()) {
375
        if ($member->_usesFileNamed($fileName)) {
376
            return _error("$fileName is needed by member "
377
                  . $member->fileName()
378
                  . "; consider using overwrite() or overwriteAs() instead.");
379
        }
380
    }
381
    my ($status, $fh) = _newFileHandle($fileName, 'w');
382
    return _ioError("Can't open $fileName for write") unless $status;
383
    my $retval = $self->writeToFileHandle($fh, 1);
384
    $fh->close();
385
    $fh = undef;
386
 
387
    return $retval;
388
}
389
 
390
# It is possible to write data to the FH before calling this,
391
# perhaps to make a self-extracting archive.
392
sub writeToFileHandle {
393
    my $self = shift;
394
 
395
    my ($fh, $fhIsSeekable);
396
    if (ref($_[0]) eq 'HASH') {
397
        $fh = $_[0]->{fileHandle};
398
        $fhIsSeekable =
399
          exists($_[0]->{seek}) ? $_[0]->{seek} : _isSeekable($fh);
400
    } else {
401
        $fh = shift;
402
        $fhIsSeekable = @_ ? shift : _isSeekable($fh);
403
    }
404
 
405
    return _error('No filehandle given')   unless $fh;
406
    return _ioError('filehandle not open') unless $fh->opened();
407
    _binmode($fh);
408
 
409
    # Find out where the current position is.
410
    my $offset = $fhIsSeekable ? $fh->tell() : 0;
411
    $offset = 0 if $offset < 0;
412
 
413
    foreach my $member ($self->members()) {
414
        my $retval = $member->_writeToFileHandle($fh, $fhIsSeekable, $offset);
415
        $member->endRead();
416
        return $retval if $retval != AZ_OK;
417
        $offset += $member->_localHeaderSize() + $member->_writeOffset();
418
        $offset +=
419
          $member->hasDataDescriptor()
420
          ? DATA_DESCRIPTOR_LENGTH + SIGNATURE_LENGTH
421
          : 0;
422
 
423
        # changed this so it reflects the last successful position
424
        $self->{'writeCentralDirectoryOffset'} = $offset;
425
    }
426
    return $self->writeCentralDirectory($fh);
427
}
428
 
429
# Write zip back to the original file,
430
# as safely as possible.
431
# Returns AZ_OK if successful.
432
sub overwrite {
433
    my $self = shift;
434
    return $self->overwriteAs($self->{'fileName'});
435
}
436
 
437
# Write zip to the specified file,
438
# as safely as possible.
439
# Returns AZ_OK if successful.
440
sub overwriteAs {
441
    my $self = shift;
442
    my $zipName = (ref($_[0]) eq 'HASH') ? $_[0]->{filename} : shift;
443
    return _error("no filename in overwriteAs()") unless defined($zipName);
444
 
445
    my ($fh, $tempName) = Archive::Zip::tempFile();
446
    return _error("Can't open temp file", $!) unless $fh;
447
 
448
    (my $backupName = $zipName) =~ s{(\.[^.]*)?$}{.zbk};
449
 
450
    my $status = $self->writeToFileHandle($fh);
451
    $fh->close();
452
    $fh = undef;
453
 
454
    if ($status != AZ_OK) {
455
        unlink($tempName);
456
        _printError("Can't write to $tempName");
457
        return $status;
458
    }
459
 
460
    my $err;
461
 
462
    # rename the zip
463
    if (-f $zipName && !rename($zipName, $backupName)) {
464
        $err = $!;
465
        unlink($tempName);
466
        return _error("Can't rename $zipName as $backupName", $err);
467
    }
468
 
469
    # move the temp to the original name (possibly copying)
470
    unless (File::Copy::move($tempName, $zipName)
471
        || File::Copy::copy($tempName, $zipName)) {
472
        $err = $!;
473
        rename($backupName, $zipName);
474
        unlink($tempName);
475
        return _error("Can't move $tempName to $zipName", $err);
476
    }
477
 
478
    # unlink the backup
479
    if (-f $backupName && !unlink($backupName)) {
480
        $err = $!;
481
        return _error("Can't unlink $backupName", $err);
482
    }
483
 
484
    return AZ_OK;
485
}
486
 
487
# Used only during writing
488
sub _writeCentralDirectoryOffset {
489
    shift->{'writeCentralDirectoryOffset'};
490
}
491
 
492
sub _writeEOCDOffset {
493
    shift->{'writeEOCDOffset'};
494
}
495
 
496
# Expects to have _writeEOCDOffset() set
497
sub _writeEndOfCentralDirectory {
498
    my ($self, $fh) = @_;
499
 
500
    $self->_print($fh, END_OF_CENTRAL_DIRECTORY_SIGNATURE_STRING)
501
      or return _ioError('writing EOCD Signature');
502
    my $zipfileCommentLength = length($self->zipfileComment());
503
 
504
    my $header = pack(
505
        END_OF_CENTRAL_DIRECTORY_FORMAT,
506
        0,                          # {'diskNumber'},
507
        0,                          # {'diskNumberWithStartOfCentralDirectory'},
508
        $self->numberOfMembers(),   # {'numberOfCentralDirectoriesOnThisDisk'},
509
        $self->numberOfMembers(),   # {'numberOfCentralDirectories'},
510
        $self->_writeEOCDOffset() - $self->_writeCentralDirectoryOffset(),
511
        $self->_writeCentralDirectoryOffset(),
512
        $zipfileCommentLength
513
    );
514
    $self->_print($fh, $header)
515
      or return _ioError('writing EOCD header');
516
    if ($zipfileCommentLength) {
517
        $self->_print($fh, $self->zipfileComment())
518
          or return _ioError('writing zipfile comment');
519
    }
520
    return AZ_OK;
521
}
522
 
523
# $offset can be specified to truncate a zip file.
524
sub writeCentralDirectory {
525
    my $self = shift;
526
 
527
    my ($fh, $offset);
528
    if (ref($_[0]) eq 'HASH') {
529
        $fh     = $_[0]->{fileHandle};
530
        $offset = $_[0]->{offset};
531
    } else {
532
        ($fh, $offset) = @_;
533
    }
534
 
535
    if (defined($offset)) {
536
        $self->{'writeCentralDirectoryOffset'} = $offset;
537
        $fh->seek($offset, IO::Seekable::SEEK_SET)
538
          or return _ioError('seeking to write central directory');
539
    } else {
540
        $offset = $self->_writeCentralDirectoryOffset();
541
    }
542
 
543
    foreach my $member ($self->members()) {
544
        my $status = $member->_writeCentralDirectoryFileHeader($fh);
545
        return $status if $status != AZ_OK;
546
        $offset += $member->_centralDirectoryHeaderSize();
547
        $self->{'writeEOCDOffset'} = $offset;
548
    }
549
    return $self->_writeEndOfCentralDirectory($fh);
550
}
551
 
552
sub read {
553
    my $self = shift;
554
    my $fileName = (ref($_[0]) eq 'HASH') ? shift->{filename} : shift;
555
    return _error('No filename given') unless $fileName;
556
    my ($status, $fh) = _newFileHandle($fileName, 'r');
557
    return _ioError("opening $fileName for read") unless $status;
558
 
559
    $status = $self->readFromFileHandle($fh, $fileName);
560
    return $status if $status != AZ_OK;
561
 
562
    $fh->close();
563
    $self->{'fileName'} = $fileName;
564
    return AZ_OK;
565
}
566
 
567
sub readFromFileHandle {
568
    my $self = shift;
569
 
570
    my ($fh, $fileName);
571
    if (ref($_[0]) eq 'HASH') {
572
        $fh       = $_[0]->{fileHandle};
573
        $fileName = $_[0]->{filename};
574
    } else {
575
        ($fh, $fileName) = @_;
576
    }
577
 
578
    $fileName = $fh unless defined($fileName);
579
    return _error('No filehandle given')   unless $fh;
580
    return _ioError('filehandle not open') unless $fh->opened();
581
 
582
    _binmode($fh);
583
    $self->{'fileName'} = "$fh";
584
 
585
    # TODO: how to support non-seekable zips?
586
    return _error('file not seekable')
587
      unless _isSeekable($fh);
588
 
589
    $fh->seek(0, 0);    # rewind the file
590
 
591
    my $status = $self->_findEndOfCentralDirectory($fh);
592
    return $status if $status != AZ_OK;
593
 
594
    my $eocdPosition = $fh->tell();
595
 
596
    $status = $self->_readEndOfCentralDirectory($fh);
597
    return $status if $status != AZ_OK;
598
 
599
    $fh->seek($eocdPosition - $self->centralDirectorySize(),
600
        IO::Seekable::SEEK_SET)
601
      or return _ioError("Can't seek $fileName");
602
 
603
    # Try to detect garbage at beginning of archives
604
    # This should be 0
605
    $self->{'eocdOffset'} = $eocdPosition - $self->centralDirectorySize() # here
606
      - $self->centralDirectoryOffsetWRTStartingDiskNumber();
607
 
608
    for (; ;) {
609
        my $newMember =
610
          Archive::Zip::Member->_newFromZipFile($fh, $fileName,
611
            $self->eocdOffset());
612
        my $signature;
613
        ($status, $signature) = _readSignature($fh, $fileName);
614
        return $status if $status != AZ_OK;
615
        last if $signature == END_OF_CENTRAL_DIRECTORY_SIGNATURE;
616
        $status = $newMember->_readCentralDirectoryFileHeader();
617
        return $status if $status != AZ_OK;
618
        $status = $newMember->endRead();
619
        return $status if $status != AZ_OK;
620
        $newMember->_becomeDirectoryIfNecessary();
621
 
622
        if(($newMember->{bitFlag} & 0x800) && !utf8::is_utf8($newMember->{fileName})){
623
            $newMember->{fileName} = Encode::decode_utf8($newMember->{fileName});
624
        }
625
 
626
        push(@{$self->{'members'}}, $newMember);
627
    }
628
 
629
    return AZ_OK;
630
}
631
 
632
# Read EOCD, starting from position before signature.
633
# Return AZ_OK on success.
634
sub _readEndOfCentralDirectory {
635
    my $self = shift;
636
    my $fh   = shift;
637
 
638
    # Skip past signature
639
    $fh->seek(SIGNATURE_LENGTH, IO::Seekable::SEEK_CUR)
640
      or return _ioError("Can't seek past EOCD signature");
641
 
642
    my $header = '';
643
    my $bytesRead = $fh->read($header, END_OF_CENTRAL_DIRECTORY_LENGTH);
644
    if ($bytesRead != END_OF_CENTRAL_DIRECTORY_LENGTH) {
645
        return _ioError("reading end of central directory");
646
    }
647
 
648
    my $zipfileCommentLength;
649
    (
650
        $self->{'diskNumber'},
651
        $self->{'diskNumberWithStartOfCentralDirectory'},
652
        $self->{'numberOfCentralDirectoriesOnThisDisk'},
653
        $self->{'numberOfCentralDirectories'},
654
        $self->{'centralDirectorySize'},
655
        $self->{'centralDirectoryOffsetWRTStartingDiskNumber'},
656
        $zipfileCommentLength
657
    ) = unpack(END_OF_CENTRAL_DIRECTORY_FORMAT, $header);
658
 
659
    if ($self->{'diskNumber'} == 0xFFFF ||
660
           $self->{'diskNumberWithStartOfCentralDirectory'} == 0xFFFF ||
661
           $self->{'numberOfCentralDirectoriesOnThisDisk'} == 0xFFFF ||
662
           $self->{'numberOfCentralDirectories'} == 0xFFFF ||
663
           $self->{'centralDirectorySize'} == 0xFFFFFFFF ||
664
           $self->{'centralDirectoryOffsetWRTStartingDiskNumber'} == 0xFFFFFFFF) {
665
        return _formatError("zip64 not supported" . Dumper($self));
666
    }
667
use Data::Dumper;
668
    if ($zipfileCommentLength) {
669
        my $zipfileComment = '';
670
        $bytesRead = $fh->read($zipfileComment, $zipfileCommentLength);
671
        if ($bytesRead != $zipfileCommentLength) {
672
            return _ioError("reading zipfile comment");
673
        }
674
        $self->{'zipfileComment'} = $zipfileComment;
675
    }
676
 
677
    return AZ_OK;
678
}
679
 
680
# Seek in my file to the end, then read backwards until we find the
681
# signature of the central directory record. Leave the file positioned right
682
# before the signature. Returns AZ_OK if success.
683
sub _findEndOfCentralDirectory {
684
    my $self = shift;
685
    my $fh   = shift;
686
    my $data = '';
687
    $fh->seek(0, IO::Seekable::SEEK_END)
688
      or return _ioError("seeking to end");
689
 
690
    my $fileLength = $fh->tell();
691
    if ($fileLength < END_OF_CENTRAL_DIRECTORY_LENGTH + 4) {
692
        return _formatError("file is too short");
693
    }
694
 
695
    my $seekOffset = 0;
696
    my $pos        = -1;
697
    for (; ;) {
698
        $seekOffset += 512;
699
        $seekOffset = $fileLength if ($seekOffset > $fileLength);
700
        $fh->seek(-$seekOffset, IO::Seekable::SEEK_END)
701
          or return _ioError("seek failed");
702
        my $bytesRead = $fh->read($data, $seekOffset);
703
        if ($bytesRead != $seekOffset) {
704
            return _ioError("read failed");
705
        }
706
        $pos = rindex($data, END_OF_CENTRAL_DIRECTORY_SIGNATURE_STRING);
707
        last
708
          if ( $pos >= 0
709
            or $seekOffset == $fileLength
710
            or $seekOffset >= $Archive::Zip::ChunkSize);
711
    }
712
 
713
    if ($pos >= 0) {
714
        $fh->seek($pos - $seekOffset, IO::Seekable::SEEK_CUR)
715
          or return _ioError("seeking to EOCD");
716
        return AZ_OK;
717
    } else {
718
        return _formatError("can't find EOCD signature");
719
    }
720
}
721
 
722
# Used to avoid taint problems when chdir'ing.
723
# Not intended to increase security in any way; just intended to shut up the -T
724
# complaints.  If your Cwd module is giving you unreliable returns from cwd()
725
# you have bigger problems than this.
726
sub _untaintDir {
727
    my $dir = shift;
728
    $dir =~ m/\A(.+)\z/s;
729
    return $1;
730
}
731
 
732
sub addTree {
733
    my $self = shift;
734
 
735
    my ($root, $dest, $pred, $compressionLevel);
736
    if (ref($_[0]) eq 'HASH') {
737
        $root             = $_[0]->{root};
738
        $dest             = $_[0]->{zipName};
739
        $pred             = $_[0]->{select};
740
        $compressionLevel = $_[0]->{compressionLevel};
741
    } else {
742
        ($root, $dest, $pred, $compressionLevel) = @_;
743
    }
744
 
745
    return _error("root arg missing in call to addTree()")
746
      unless defined($root);
747
    $dest = '' unless defined($dest);
748
    $pred = sub { -r }
749
      unless defined($pred);
750
 
751
    my @files;
752
    my $startDir = _untaintDir(cwd());
753
 
754
    return _error('undef returned by _untaintDir on cwd ', cwd())
755
      unless $startDir;
756
 
757
    # This avoids chdir'ing in Find, in a way compatible with older
758
    # versions of File::Find.
759
    my $wanted = sub {
760
        local $main::_ = $File::Find::name;
761
        my $dir = _untaintDir($File::Find::dir);
762
        chdir($startDir);
763
        if ($^O eq 'MSWin32' && $Archive::Zip::UNICODE) {
764
            push(@files, Win32::GetANSIPathName($File::Find::name)) if (&$pred);
765
            $dir = Win32::GetANSIPathName($dir);
766
        } else {
767
            push(@files, $File::Find::name) if (&$pred);
768
        }
769
        chdir($dir);
770
    };
771
 
772
    if ($^O eq 'MSWin32' && $Archive::Zip::UNICODE) {
773
        $root = Win32::GetANSIPathName($root);
774
    }
775
    File::Find::find($wanted, $root);
776
 
777
    my $rootZipName = _asZipDirName($root, 1);    # with trailing slash
778
    my $pattern = $rootZipName eq './' ? '^' : "^\Q$rootZipName\E";
779
 
780
    $dest = _asZipDirName($dest, 1);              # with trailing slash
781
 
782
    foreach my $fileName (@files) {
783
        my $isDir;
784
        if ($^O eq 'MSWin32' && $Archive::Zip::UNICODE) {
785
            $isDir = -d Win32::GetANSIPathName($fileName);
786
        } else {
787
            $isDir = -d $fileName;
788
        }
789
 
790
        # normalize, remove leading ./
791
        my $archiveName = _asZipDirName($fileName, $isDir);
792
        if ($archiveName eq $rootZipName) { $archiveName = $dest }
793
        else                              { $archiveName =~ s{$pattern}{$dest} }
794
        next if $archiveName =~ m{^\.?/?$};    # skip current dir
795
        my $member =
796
            $isDir
797
          ? $self->addDirectory($fileName, $archiveName)
798
          : $self->addFile($fileName, $archiveName);
799
        $member->desiredCompressionLevel($compressionLevel);
800
 
801
        return _error("add $fileName failed in addTree()") if !$member;
802
    }
803
    return AZ_OK;
804
}
805
 
806
sub addTreeMatching {
807
    my $self = shift;
808
 
809
    my ($root, $dest, $pattern, $pred, $compressionLevel);
810
    if (ref($_[0]) eq 'HASH') {
811
        $root             = $_[0]->{root};
812
        $dest             = $_[0]->{zipName};
813
        $pattern          = $_[0]->{pattern};
814
        $pred             = $_[0]->{select};
815
        $compressionLevel = $_[0]->{compressionLevel};
816
    } else {
817
        ($root, $dest, $pattern, $pred, $compressionLevel) = @_;
818
    }
819
 
820
    return _error("root arg missing in call to addTreeMatching()")
821
      unless defined($root);
822
    $dest = '' unless defined($dest);
823
    return _error("pattern missing in call to addTreeMatching()")
824
      unless defined($pattern);
825
    my $matcher =
826
      $pred ? sub { m{$pattern} && &$pred } : sub { m{$pattern} && -r };
827
    return $self->addTree($root, $dest, $matcher, $compressionLevel);
828
}
829
 
830
# $zip->extractTree( $root, $dest [, $volume] );
831
#
832
# $root and $dest are Unix-style.
833
# $volume is in local FS format.
834
#
835
sub extractTree {
836
    my $self = shift;
837
 
838
    my ($root, $dest, $volume);
839
    if (ref($_[0]) eq 'HASH') {
840
        $root   = $_[0]->{root};
841
        $dest   = $_[0]->{zipName};
842
        $volume = $_[0]->{volume};
843
    } else {
844
        ($root, $dest, $volume) = @_;
845
    }
846
 
847
    $root = '' unless defined($root);
848
    if (defined $dest) {
849
        if ($dest !~ m{/$}) {
850
            $dest .= '/';
851
        }
852
    } else {
853
        $dest = './';
854
    }
855
 
856
    my $pattern = "^\Q$root";
857
    my @members = $self->membersMatching($pattern);
858
 
859
    foreach my $member (@members) {
860
        my $fileName = $member->fileName();    # in Unix format
861
        $fileName =~ s{$pattern}{$dest};       # in Unix format
862
                                               # convert to platform format:
863
        $fileName = Archive::Zip::_asLocalName($fileName, $volume);
864
        my $status = $member->extractToFileNamed($fileName);
865
        return $status if $status != AZ_OK;
866
    }
867
    return AZ_OK;
868
}
869
 
870
# $zip->updateMember( $memberOrName, $fileName );
871
# Returns (possibly updated) member, if any; undef on errors.
872
 
873
sub updateMember {
874
    my $self = shift;
875
 
876
    my ($oldMember, $fileName);
877
    if (ref($_[0]) eq 'HASH') {
878
        $oldMember = $_[0]->{memberOrZipName};
879
        $fileName  = $_[0]->{name};
880
    } else {
881
        ($oldMember, $fileName) = @_;
882
    }
883
 
884
    if (!defined($fileName)) {
885
        _error("updateMember(): missing fileName argument");
886
        return undef;
887
    }
888
 
889
    my @newStat = stat($fileName);
890
    if (!@newStat) {
891
        _ioError("Can't stat $fileName");
892
        return undef;
893
    }
894
 
895
    my $isDir = -d _;
896
 
897
    my $memberName;
898
 
899
    if (ref($oldMember)) {
900
        $memberName = $oldMember->fileName();
901
    } else {
902
        $oldMember = $self->memberNamed($memberName = $oldMember)
903
          || $self->memberNamed($memberName =
904
              _asZipDirName($oldMember, $isDir));
905
    }
906
 
907
    unless (defined($oldMember)
908
        && $oldMember->lastModTime() == $newStat[9]
909
        && $oldMember->isDirectory() == $isDir
910
        && ($isDir || ($oldMember->uncompressedSize() == $newStat[7]))) {
911
 
912
        # create the new member
913
        my $newMember =
914
            $isDir
915
          ? Archive::Zip::Member->newDirectoryNamed($fileName, $memberName)
916
          : Archive::Zip::Member->newFromFile($fileName, $memberName);
917
 
918
        unless (defined($newMember)) {
919
            _error("creation of member $fileName failed in updateMember()");
920
            return undef;
921
        }
922
 
923
        # replace old member or append new one
924
        if (defined($oldMember)) {
925
            $self->replaceMember($oldMember, $newMember);
926
        } else {
927
            $self->addMember($newMember);
928
        }
929
 
930
        return $newMember;
931
    }
932
 
933
    return $oldMember;
934
}
935
 
936
# $zip->updateTree( $root, [ $dest, [ $pred [, $mirror]]] );
937
#
938
# This takes the same arguments as addTree, but first checks to see
939
# whether the file or directory already exists in the zip file.
940
#
941
# If the fourth argument $mirror is true, then delete all my members
942
# if corresponding files were not found.
943
 
944
sub updateTree {
945
    my $self = shift;
946
 
947
    my ($root, $dest, $pred, $mirror, $compressionLevel);
948
    if (ref($_[0]) eq 'HASH') {
949
        $root             = $_[0]->{root};
950
        $dest             = $_[0]->{zipName};
951
        $pred             = $_[0]->{select};
952
        $mirror           = $_[0]->{mirror};
953
        $compressionLevel = $_[0]->{compressionLevel};
954
    } else {
955
        ($root, $dest, $pred, $mirror, $compressionLevel) = @_;
956
    }
957
 
958
    return _error("root arg missing in call to updateTree()")
959
      unless defined($root);
960
    $dest = '' unless defined($dest);
961
    $pred = sub { -r }
962
      unless defined($pred);
963
 
964
    $dest = _asZipDirName($dest, 1);
965
    my $rootZipName = _asZipDirName($root, 1);    # with trailing slash
966
    my $pattern = $rootZipName eq './' ? '^' : "^\Q$rootZipName\E";
967
 
968
    my @files;
969
    my $startDir = _untaintDir(cwd());
970
 
971
    return _error('undef returned by _untaintDir on cwd ', cwd())
972
      unless $startDir;
973
 
974
    # This avoids chdir'ing in Find, in a way compatible with older
975
    # versions of File::Find.
976
    my $wanted = sub {
977
        local $main::_ = $File::Find::name;
978
        my $dir = _untaintDir($File::Find::dir);
979
        chdir($startDir);
980
        push(@files, $File::Find::name) if (&$pred);
981
        chdir($dir);
982
    };
983
 
984
    File::Find::find($wanted, $root);
985
 
986
    # Now @files has all the files that I could potentially be adding to
987
    # the zip. Only add the ones that are necessary.
988
    # For each file (updated or not), add its member name to @done.
989
    my %done;
990
    foreach my $fileName (@files) {
991
        my @newStat = stat($fileName);
992
        my $isDir   = -d _;
993
 
994
        # normalize, remove leading ./
995
        my $memberName = _asZipDirName($fileName, $isDir);
996
        if ($memberName eq $rootZipName) { $memberName = $dest }
997
        else                             { $memberName =~ s{$pattern}{$dest} }
998
        next if $memberName =~ m{^\.?/?$};    # skip current dir
999
 
1000
        $done{$memberName} = 1;
1001
        my $changedMember = $self->updateMember($memberName, $fileName);
1002
        $changedMember->desiredCompressionLevel($compressionLevel);
1003
        return _error("updateTree failed to update $fileName")
1004
          unless ref($changedMember);
1005
    }
1006
 
1007
    # @done now has the archive names corresponding to all the found files.
1008
    # If we're mirroring, delete all those members that aren't in @done.
1009
    if ($mirror) {
1010
        foreach my $member ($self->members()) {
1011
            $self->removeMember($member)
1012
              unless $done{$member->fileName()};
1013
        }
1014
    }
1015
 
1016
    return AZ_OK;
1017
}
1018
 
1019
1;