diff options
Diffstat (limited to 'lib/Archive/Zip/ZipFileMember.pm')
-rw-r--r-- | lib/Archive/Zip/ZipFileMember.pm | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/lib/Archive/Zip/ZipFileMember.pm b/lib/Archive/Zip/ZipFileMember.pm new file mode 100644 index 0000000..cd4c875 --- /dev/null +++ b/lib/Archive/Zip/ZipFileMember.pm @@ -0,0 +1,416 @@ +package Archive::Zip::ZipFileMember; + +use strict; +use vars qw( $VERSION @ISA ); + +BEGIN { + $VERSION = '1.49'; + @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 is not, 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; + + # The buffer withe encrypted data is prefixed with a new + # encrypted 12 byte header. The size only changes when + # the buffer is also compressed + $self->isEncrypted && $oldUncompressedSize > $self->{uncompressedSize} + and $oldUncompressedSize -= DATA_DESCRIPTOR_LENGTH; + + return _formatError( + "CRC or size mismatch while skipping data descriptor") + if ( $oldCrc32 != $self->{'crc32'} + || $oldUncompressedSize != $self->{'uncompressedSize'}); + + $self->{'crc32'} = 0 + if $self->compressionMethod() == COMPRESSION_STORED ; + } + + 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 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; |