summaryrefslogtreecommitdiff
path: root/Master/tlpkg/tlperl.straw/lib/Archive/Zip/ZipFileMember.pm
blob: cc606697b1547909bac0d35db3e9a3e644f56ea9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
package Archive::Zip::ZipFileMember;

use strict;
use vars qw( $VERSION @ISA );

BEGIN {
    $VERSION = '1.30';
    @ISA     = qw ( Archive::Zip::FileMember );
}

use Archive::Zip qw(
  :CONSTANTS
  :ERROR_CODES
  :PKZIP_CONSTANTS
  :UTILITY_METHODS
);

# Create a new Archive::Zip::ZipFileMember
# given a filename and optional open file handle
#
sub _newFromZipFile {
    my $class              = shift;
    my $fh                 = shift;
    my $externalFileName   = shift;
    my $possibleEocdOffset = shift;    # normally 0

    my $self = $class->new(
        'crc32'                     => 0,
        'diskNumberStart'           => 0,
        'localHeaderRelativeOffset' => 0,
        'dataOffset' => 0,    # localHeaderRelativeOffset + header length
        @_
    );
    $self->{'externalFileName'}   = $externalFileName;
    $self->{'fh'}                 = $fh;
    $self->{'possibleEocdOffset'} = $possibleEocdOffset;
    return $self;
}

sub isDirectory {
    my $self = shift;
    return (
        substr( $self->fileName, -1, 1 ) eq '/'
        and
        $self->uncompressedSize == 0
    );
}

# Seek to the beginning of the local header, just past the signature.
# Verify that the local header signature is in fact correct.
# Update the localHeaderRelativeOffset if necessary by adding the possibleEocdOffset.
# Returns status.

sub _seekToLocalHeader {
    my $self          = shift;
    my $where         = shift;    # optional
    my $previousWhere = shift;    # optional

    $where = $self->localHeaderRelativeOffset() unless defined($where);

    # avoid loop on certain corrupt files (from Julian Field)
    return _formatError("corrupt zip file")
      if defined($previousWhere) && $where == $previousWhere;

    my $status;
    my $signature;

    $status = $self->fh()->seek( $where, IO::Seekable::SEEK_SET );
    return _ioError("seeking to local header") unless $status;

    ( $status, $signature ) =
      _readSignature( $self->fh(), $self->externalFileName(),
        LOCAL_FILE_HEADER_SIGNATURE );
    return $status if $status == AZ_IO_ERROR;

    # retry with EOCD offset if any was given.
    if ( $status == AZ_FORMAT_ERROR && $self->{'possibleEocdOffset'} ) {
        $status = $self->_seekToLocalHeader(
            $self->localHeaderRelativeOffset() + $self->{'possibleEocdOffset'},
            $where
        );
        if ( $status == AZ_OK ) {
            $self->{'localHeaderRelativeOffset'} +=
              $self->{'possibleEocdOffset'};
            $self->{'possibleEocdOffset'} = 0;
        }
    }

    return $status;
}

# Because I'm going to delete the file handle, read the local file
# header if the file handle is seekable. If it isn't, I assume that
# I've already read the local header.
# Return ( $status, $self )

sub _become {
    my $self     = shift;
    my $newClass = shift;
    return $self if ref($self) eq $newClass;

    my $status = AZ_OK;

    if ( _isSeekable( $self->fh() ) ) {
        my $here = $self->fh()->tell();
        $status = $self->_seekToLocalHeader();
        $status = $self->_readLocalFileHeader() if $status == AZ_OK;
        $self->fh()->seek( $here, IO::Seekable::SEEK_SET );
        return $status unless $status == AZ_OK;
    }

    delete( $self->{'eocdCrc32'} );
    delete( $self->{'diskNumberStart'} );
    delete( $self->{'localHeaderRelativeOffset'} );
    delete( $self->{'dataOffset'} );

    return $self->SUPER::_become($newClass);
}

sub diskNumberStart {
    shift->{'diskNumberStart'};
}

sub localHeaderRelativeOffset {
    shift->{'localHeaderRelativeOffset'};
}

sub dataOffset {
    shift->{'dataOffset'};
}

# Skip local file header, updating only extra field stuff.
# Assumes that fh is positioned before signature.
sub _skipLocalFileHeader {
    my $self = shift;
    my $header;
    my $bytesRead = $self->fh()->read( $header, LOCAL_FILE_HEADER_LENGTH );
    if ( $bytesRead != LOCAL_FILE_HEADER_LENGTH ) {
        return _ioError("reading local file header");
    }
    my $fileNameLength;
    my $extraFieldLength;
    my $bitFlag;
    (
        undef,    # $self->{'versionNeededToExtract'},
        $bitFlag,
        undef,    # $self->{'compressionMethod'},
        undef,    # $self->{'lastModFileDateTime'},
        undef,    # $crc32,
        undef,    # $compressedSize,
        undef,    # $uncompressedSize,
        $fileNameLength,
        $extraFieldLength
    ) = unpack( LOCAL_FILE_HEADER_FORMAT, $header );

    if ($fileNameLength) {
        $self->fh()->seek( $fileNameLength, IO::Seekable::SEEK_CUR )
          or return _ioError("skipping local file name");
    }

    if ($extraFieldLength) {
        $bytesRead =
          $self->fh()->read( $self->{'localExtraField'}, $extraFieldLength );
        if ( $bytesRead != $extraFieldLength ) {
            return _ioError("reading local extra field");
        }
    }

    $self->{'dataOffset'} = $self->fh()->tell();

    if ( $bitFlag & GPBF_HAS_DATA_DESCRIPTOR_MASK ) {

        # Read the crc32, compressedSize, and uncompressedSize from the
        # extended data descriptor, which directly follows the compressed data.
        #
        # Skip over the compressed file data (assumes that EOCD compressedSize
        # was correct)
        $self->fh()->seek( $self->{'compressedSize'}, IO::Seekable::SEEK_CUR )
          or return _ioError("seeking to extended local header");

        # these values should be set correctly from before.
        my $oldCrc32            = $self->{'eocdCrc32'};
        my $oldCompressedSize   = $self->{'compressedSize'};
        my $oldUncompressedSize = $self->{'uncompressedSize'};

        my $status = $self->_readDataDescriptor();
        return $status unless $status == AZ_OK;

        return _formatError(
            "CRC or size mismatch while skipping data descriptor")
          if ( $oldCrc32 != $self->{'crc32'}
            || $oldUncompressedSize != $self->{'uncompressedSize'} );
    }

    return AZ_OK;
}

# Read from a local file header into myself. Returns AZ_OK if successful.
# Assumes that fh is positioned after signature.
# Note that crc32, compressedSize, and uncompressedSize will be 0 if
# GPBF_HAS_DATA_DESCRIPTOR_MASK is set in the bitFlag.

sub _readLocalFileHeader {
    my $self = shift;
    my $header;
    my $bytesRead = $self->fh()->read( $header, LOCAL_FILE_HEADER_LENGTH );
    if ( $bytesRead != LOCAL_FILE_HEADER_LENGTH ) {
        return _ioError("reading local file header");
    }
    my $fileNameLength;
    my $crc32;
    my $compressedSize;
    my $uncompressedSize;
    my $extraFieldLength;
    (
        $self->{'versionNeededToExtract'}, $self->{'bitFlag'},
        $self->{'compressionMethod'},      $self->{'lastModFileDateTime'},
        $crc32,                            $compressedSize,
        $uncompressedSize,                 $fileNameLength,
        $extraFieldLength
    ) = unpack( LOCAL_FILE_HEADER_FORMAT, $header );

    if ($fileNameLength) {
        my $fileName;
        $bytesRead = $self->fh()->read( $fileName, $fileNameLength );
        if ( $bytesRead != $fileNameLength ) {
            return _ioError("reading local file name");
        }
        $self->fileName($fileName);
    }

    if ($extraFieldLength) {
        $bytesRead =
          $self->fh()->read( $self->{'localExtraField'}, $extraFieldLength );
        if ( $bytesRead != $extraFieldLength ) {
            return _ioError("reading local extra field");
        }
    }

    $self->{'dataOffset'} = $self->fh()->tell();

    if ( $self->hasDataDescriptor() ) {

        # Read the crc32, compressedSize, and uncompressedSize from the
        # extended data descriptor.
        # Skip over the compressed file data (assumes that EOCD compressedSize
        # was correct)
        $self->fh()->seek( $self->{'compressedSize'}, IO::Seekable::SEEK_CUR )
          or return _ioError("seeking to extended local header");

        my $status = $self->_readDataDescriptor();
        return $status unless $status == AZ_OK;
    }
    else {
        return _formatError(
            "CRC or size mismatch after reading data descriptor")
          if ( $self->{'crc32'} != $crc32
            || $self->{'uncompressedSize'} != $uncompressedSize );
    }

    return AZ_OK;
}

# This will read the data descriptor, which is after the end of compressed file
# data in members that that have GPBF_HAS_DATA_DESCRIPTOR_MASK set in their
# bitFlag.
# The only reliable way to find these is to rely on the EOCD compressedSize.
# Assumes that file is positioned immediately after the compressed data.
# Returns status; sets crc32, compressedSize, and uncompressedSize.
sub _readDataDescriptor {
    my $self = shift;
    my $signatureData;
    my $header;
    my $crc32;
    my $compressedSize;
    my $uncompressedSize;

    my $bytesRead = $self->fh()->read( $signatureData, SIGNATURE_LENGTH );
    return _ioError("reading header signature")
      if $bytesRead != SIGNATURE_LENGTH;
    my $signature = unpack( SIGNATURE_FORMAT, $signatureData );

    # unfortunately, the signature appears to be optional.
    if ( $signature == DATA_DESCRIPTOR_SIGNATURE
        && ( $signature != $self->{'crc32'} ) )
    {
        $bytesRead = $self->fh()->read( $header, DATA_DESCRIPTOR_LENGTH );
        return _ioError("reading data descriptor")
          if $bytesRead != DATA_DESCRIPTOR_LENGTH;

        ( $crc32, $compressedSize, $uncompressedSize ) =
          unpack( DATA_DESCRIPTOR_FORMAT, $header );
    }
    else {
        $bytesRead =
          $self->fh()->read( $header, DATA_DESCRIPTOR_LENGTH_NO_SIG );
        return _ioError("reading data descriptor")
          if $bytesRead != DATA_DESCRIPTOR_LENGTH_NO_SIG;

        $crc32 = $signature;
        ( $compressedSize, $uncompressedSize ) =
          unpack( DATA_DESCRIPTOR_FORMAT_NO_SIG, $header );
    }

    $self->{'eocdCrc32'} = $self->{'crc32'}
      unless defined( $self->{'eocdCrc32'} );
    $self->{'crc32'}            = $crc32;
    $self->{'compressedSize'}   = $compressedSize;
    $self->{'uncompressedSize'} = $uncompressedSize;

    return AZ_OK;
}

# Read a Central Directory header. Return AZ_OK on success.
# Assumes that fh is positioned right after the signature.

sub _readCentralDirectoryFileHeader {
    my $self      = shift;
    my $fh        = $self->fh();
    my $header    = '';
    my $bytesRead = $fh->read( $header, CENTRAL_DIRECTORY_FILE_HEADER_LENGTH );
    if ( $bytesRead != CENTRAL_DIRECTORY_FILE_HEADER_LENGTH ) {
        return _ioError("reading central dir header");
    }
    my ( $fileNameLength, $extraFieldLength, $fileCommentLength );
    (
        $self->{'versionMadeBy'},
        $self->{'fileAttributeFormat'},
        $self->{'versionNeededToExtract'},
        $self->{'bitFlag'},
        $self->{'compressionMethod'},
        $self->{'lastModFileDateTime'},
        $self->{'crc32'},
        $self->{'compressedSize'},
        $self->{'uncompressedSize'},
        $fileNameLength,
        $extraFieldLength,
        $fileCommentLength,
        $self->{'diskNumberStart'},
        $self->{'internalFileAttributes'},
        $self->{'externalFileAttributes'},
        $self->{'localHeaderRelativeOffset'}
    ) = unpack( CENTRAL_DIRECTORY_FILE_HEADER_FORMAT, $header );

    $self->{'eocdCrc32'} = $self->{'crc32'};

    if ($fileNameLength) {
        $bytesRead = $fh->read( $self->{'fileName'}, $fileNameLength );
        if ( $bytesRead != $fileNameLength ) {
            _ioError("reading central dir filename");
        }
    }
    if ($extraFieldLength) {
        $bytesRead = $fh->read( $self->{'cdExtraField'}, $extraFieldLength );
        if ( $bytesRead != $extraFieldLength ) {
            return _ioError("reading central dir extra field");
        }
    }
    if ($fileCommentLength) {
        $bytesRead = $fh->read( $self->{'fileComment'}, $fileCommentLength );
        if ( $bytesRead != $fileCommentLength ) {
            return _ioError("reading central dir file comment");
        }
    }

    # NK 10/21/04: added to avoid problems with manipulated headers
    if (    $self->{'uncompressedSize'} != $self->{'compressedSize'}
        and $self->{'compressionMethod'} == COMPRESSION_STORED )
    {
        $self->{'uncompressedSize'} = $self->{'compressedSize'};
    }

    $self->desiredCompressionMethod( $self->compressionMethod() );

    return AZ_OK;
}

sub rewindData {
    my $self = shift;

    my $status = $self->SUPER::rewindData(@_);
    return $status unless $status == AZ_OK;

    return AZ_IO_ERROR unless $self->fh();

    $self->fh()->clearerr();

    # Seek to local file header.
    # The only reason that I'm doing this this way is that the extraField
    # length seems to be different between the CD header and the LF header.
    $status = $self->_seekToLocalHeader();
    return $status unless $status == AZ_OK;

    # skip local file header
    $status = $self->_skipLocalFileHeader();
    return $status unless $status == AZ_OK;

    # Seek to beginning of file data
    $self->fh()->seek( $self->dataOffset(), IO::Seekable::SEEK_SET )
      or return _ioError("seeking to beginning of file data");

    return AZ_OK;
}

# Return bytes read. Note that first parameter is a ref to a buffer.
# my $data;
# my ( $bytesRead, $status) = $self->readRawChunk( \$data, $chunkSize );
sub _readRawChunk {
    my ( $self, $dataRef, $chunkSize ) = @_;
    return ( 0, AZ_OK ) unless $chunkSize;
    my $bytesRead = $self->fh()->read( $$dataRef, $chunkSize )
      or return ( 0, _ioError("reading data") );
    return ( $bytesRead, AZ_OK );
}

1;