package IO::Compress::Zip ; use strict ; use warnings; use bytes; use IO::Compress::Base::Common 2.008 qw(:Status createSelfTiedObject); use IO::Compress::RawDeflate 2.008 ; use IO::Compress::Adapter::Deflate 2.008 ; use IO::Compress::Adapter::Identity 2.008 ; use IO::Compress::Zlib::Extra 2.008 ; use IO::Compress::Zip::Constants 2.008 ; use Compress::Raw::Zlib 2.008 qw(crc32) ; BEGIN { eval { require IO::Compress::Adapter::Bzip2 ; import IO::Compress::Adapter::Bzip2 2.008 ; require IO::Compress::Bzip2 ; import IO::Compress::Bzip2 2.008 ; } ; } require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, $ZipError); $VERSION = '2.008'; $ZipError = ''; @ISA = qw(Exporter IO::Compress::RawDeflate); @EXPORT_OK = qw( $ZipError zip ) ; %EXPORT_TAGS = %IO::Compress::RawDeflate::DEFLATE_CONSTANTS ; push @{ $EXPORT_TAGS{all} }, @EXPORT_OK ; $EXPORT_TAGS{zip_method} = [qw( ZIP_CM_STORE ZIP_CM_DEFLATE ZIP_CM_BZIP2 )]; push @{ $EXPORT_TAGS{all} }, @{ $EXPORT_TAGS{zip_method} }; Exporter::export_ok_tags('all'); sub new { my $class = shift ; my $obj = createSelfTiedObject($class, \$ZipError); $obj->_create(undef, @_); } sub zip { my $obj = createSelfTiedObject(undef, \$ZipError); return $obj->_def(@_); } sub mkComp { my $self = shift ; my $class = shift ; my $got = shift ; my ($obj, $errstr, $errno) ; if (*$self->{ZipData}{Method} == ZIP_CM_STORE) { ($obj, $errstr, $errno) = IO::Compress::Adapter::Identity::mkCompObject( $got->value('Level'), $got->value('Strategy') ); } elsif (*$self->{ZipData}{Method} == ZIP_CM_DEFLATE) { ($obj, $errstr, $errno) = IO::Compress::Adapter::Deflate::mkCompObject( $got->value('CRC32'), $got->value('Adler32'), $got->value('Level'), $got->value('Strategy') ); } elsif (*$self->{ZipData}{Method} == ZIP_CM_BZIP2) { ($obj, $errstr, $errno) = IO::Compress::Adapter::Bzip2::mkCompObject( $got->value('BlockSize100K'), $got->value('WorkFactor'), $got->value('Verbosity') ); *$self->{ZipData}{CRC32} = crc32(undef); } return $self->saveErrorString(undef, $errstr, $errno) if ! defined $obj; if (! defined *$self->{ZipData}{StartOffset}) { *$self->{ZipData}{StartOffset} = 0; *$self->{ZipData}{Offset} = new U64 ; } return $obj; } sub reset { my $self = shift ; *$self->{Compress}->reset(); *$self->{ZipData}{CRC32} = Compress::Raw::Zlib::crc32(''); return STATUS_OK; } sub filterUncompressed { my $self = shift ; if (*$self->{ZipData}{Method} == ZIP_CM_DEFLATE) { *$self->{ZipData}{CRC32} = *$self->{Compress}->crc32(); } else { *$self->{ZipData}{CRC32} = crc32(${$_[0]}, *$self->{ZipData}{CRC32}); } } sub mkHeader { my $self = shift; my $param = shift ; *$self->{ZipData}{StartOffset} = *$self->{ZipData}{Offset}->get32bit() ; my $filename = ''; $filename = $param->value('Name') || ''; my $comment = ''; $comment = $param->value('Comment') || ''; my $hdr = ''; my $time = _unixToDosTime($param->value('Time')); my $extra = ''; my $ctlExtra = ''; my $empty = 0; my $osCode = $param->value('OS_Code') ; my $extFileAttr = 0 ; if (*$self->{ZipData}{Zip64}) { $empty = 0xFFFF; my $x = ''; $x .= pack "V V", 0, 0 ; # uncompressedLength $x .= pack "V V", 0, 0 ; # compressedLength $x .= *$self->{ZipData}{Offset}->getPacked_V64() ; # offset to local hdr #$x .= pack "V ", 0 ; # disk no $x = IO::Compress::Zlib::Extra::mkSubField(ZIP_EXTRA_ID_ZIP64, $x); $extra .= $x; $ctlExtra .= $x; } if (! $param->value('Minimal')) { if (defined $param->value('exTime')) { $extra .= mkExtendedTime($param->value('MTime'), $param->value('ATime'), $param->value('CTime')); $ctlExtra .= mkExtendedTime($param->value('MTime')); } if ( $param->value('UID') && $osCode == ZIP_OS_CODE_UNIX) { $extra .= mkUnix2Extra( $param->value('UID'), $param->value('GID')); $ctlExtra .= mkUnix2Extra(); } # TODO - this code assumes Unix. #$extFileAttr = 0666 << 16 # if $osCode == ZIP_OS_CODE_UNIX ; $extFileAttr = $param->value('ExtAttr') if defined $param->value('ExtAttr') ; $extra .= $param->value('ExtraFieldLocal') if defined $param->value('ExtraFieldLocal'); $ctlExtra .= $param->value('ExtraFieldCentral') if defined $param->value('ExtraFieldCentral'); } my $gpFlag = 0 ; $gpFlag |= ZIP_GP_FLAG_STREAMING_MASK if *$self->{ZipData}{Stream} ; my $method = *$self->{ZipData}{Method} ; my $version = $ZIP_CM_MIN_VERSIONS{$method}; $version = ZIP64_MIN_VERSION if ZIP64_MIN_VERSION > $version && *$self->{ZipData}{Zip64}; my $madeBy = ($param->value('OS_Code') << 8) + $version; my $extract = $version; *$self->{ZipData}{Version} = $version; *$self->{ZipData}{MadeBy} = $madeBy; my $ifa = 0; $ifa |= ZIP_IFA_TEXT_MASK if $param->value('TextFlag'); $hdr .= pack "V", ZIP_LOCAL_HDR_SIG ; # signature $hdr .= pack 'v', $extract ; # extract Version & OS $hdr .= pack 'v', $gpFlag ; # general purpose flag (set streaming mode) $hdr .= pack 'v', $method ; # compression method (deflate) $hdr .= pack 'V', $time ; # last mod date/time $hdr .= pack 'V', 0 ; # crc32 - 0 when streaming $hdr .= pack 'V', $empty ; # compressed length - 0 when streaming $hdr .= pack 'V', $empty ; # uncompressed length - 0 when streaming $hdr .= pack 'v', length $filename ; # filename length $hdr .= pack 'v', length $extra ; # extra length $hdr .= $filename ; $hdr .= $extra ; my $ctl = ''; $ctl .= pack "V", ZIP_CENTRAL_HDR_SIG ; # signature $ctl .= pack 'v', $madeBy ; # version made by $ctl .= pack 'v', $extract ; # extract Version $ctl .= pack 'v', $gpFlag ; # general purpose flag (streaming mode) $ctl .= pack 'v', $method ; # compression method (deflate) $ctl .= pack 'V', $time ; # last mod date/time $ctl .= pack 'V', 0 ; # crc32 $ctl .= pack 'V', $empty ; # compressed length $ctl .= pack 'V', $empty ; # uncompressed length $ctl .= pack 'v', length $filename ; # filename length $ctl .= pack 'v', length $ctlExtra ; # extra length $ctl .= pack 'v', length $comment ; # file comment length $ctl .= pack 'v', 0 ; # disk number start $ctl .= pack 'v', $ifa ; # internal file attributes $ctl .= pack 'V', $extFileAttr ; # external file attributes if (! *$self->{ZipData}{Zip64}) { $ctl .= pack 'V', *$self->{ZipData}{Offset}->get32bit() ; # offset to local header } else { $ctl .= pack 'V', $empty ; # offset to local header } $ctl .= $filename ; *$self->{ZipData}{StartOffset64} = 4 + length $ctl; $ctl .= $ctlExtra ; $ctl .= $comment ; *$self->{ZipData}{Offset}->add(length $hdr) ; *$self->{ZipData}{CentralHeader} = $ctl; return $hdr; } sub mkTrailer { my $self = shift ; my $crc32 ; if (*$self->{ZipData}{Method} == ZIP_CM_DEFLATE) { $crc32 = pack "V", *$self->{Compress}->crc32(); } else { $crc32 = pack "V", *$self->{ZipData}{CRC32}; } my $ctl = *$self->{ZipData}{CentralHeader} ; my $sizes ; if (! *$self->{ZipData}{Zip64}) { $sizes .= *$self->{CompSize}->getPacked_V32() ; # Compressed size $sizes .= *$self->{UnCompSize}->getPacked_V32() ; # Uncompressed size } else { $sizes .= *$self->{CompSize}->getPacked_V64() ; # Compressed size $sizes .= *$self->{UnCompSize}->getPacked_V64() ; # Uncompressed size } my $data = $crc32 . $sizes ; my $hdr = ''; if (*$self->{ZipData}{Stream}) { $hdr = pack "V", ZIP_DATA_HDR_SIG ; # signature $hdr .= $data ; } else { $self->writeAt(*$self->{ZipData}{StartOffset} + 14, $data) or return undef; } if (! *$self->{ZipData}{Zip64}) { substr($ctl, 16, length $data) = $data } else { substr($ctl, 16, length $crc32) = $crc32 ; my $s = *$self->{UnCompSize}->getPacked_V64() ; # Uncompressed size $s .= *$self->{CompSize}->getPacked_V64() ; # Compressed size substr($ctl, *$self->{ZipData}{StartOffset64}, length $s) = $s ; } *$self->{ZipData}{Offset}->add(length($hdr)); *$self->{ZipData}{Offset}->add( *$self->{CompSize} ); push @{ *$self->{ZipData}{CentralDir} }, $ctl ; return $hdr; } sub mkFinalTrailer { my $self = shift ; my $comment = ''; $comment = *$self->{ZipData}{ZipComment} ; my $cd_offset = *$self->{ZipData}{Offset}->get32bit() ; # offset to start central dir my $entries = @{ *$self->{ZipData}{CentralDir} }; my $cd = join '', @{ *$self->{ZipData}{CentralDir} }; my $cd_len = length $cd ; my $z64e = ''; if ( *$self->{ZipData}{Zip64} ) { my $v = *$self->{ZipData}{Version} ; my $mb = *$self->{ZipData}{MadeBy} ; $z64e .= pack 'v', $v ; # Version made by $z64e .= pack 'v', $mb ; # Version to extract $z64e .= pack 'V', 0 ; # number of disk $z64e .= pack 'V', 0 ; # number of disk with central dir $z64e .= U64::pack_V64 $entries ; # entries in central dir on this disk $z64e .= U64::pack_V64 $entries ; # entries in central dir $z64e .= U64::pack_V64 $cd_len ; # size of central dir $z64e .= *$self->{ZipData}{Offset}->getPacked_V64() ; # offset to start central dir $z64e = pack("V", ZIP64_END_CENTRAL_REC_HDR_SIG) # signature . U64::pack_V64(length $z64e) . $z64e ; *$self->{ZipData}{Offset}->add(length $cd) ; $z64e .= pack "V", ZIP64_END_CENTRAL_LOC_HDR_SIG; # signature $z64e .= pack 'V', 0 ; # number of disk with central dir $z64e .= *$self->{ZipData}{Offset}->getPacked_V64() ; # offset to end zip64 central dir $z64e .= pack 'V', 1 ; # Total number of disks # TODO - fix these when info-zip 3 is fixed. #$cd_len = #$cd_offset = $entries = 0xFFFF ; } my $ecd = ''; $ecd .= pack "V", ZIP_END_CENTRAL_HDR_SIG ; # signature $ecd .= pack 'v', 0 ; # number of disk $ecd .= pack 'v', 0 ; # number of disk with central dir $ecd .= pack 'v', $entries ; # entries in central dir on this disk $ecd .= pack 'v', $entries ; # entries in central dir $ecd .= pack 'V', $cd_len ; # size of central dir $ecd .= pack 'V', $cd_offset ; # offset to start central dir $ecd .= pack 'v', length $comment ; # zipfile comment length $ecd .= $comment; return $cd . $z64e . $ecd ; } sub ckParams { my $self = shift ; my $got = shift; $got->value('CRC32' => 1); if (! $got->parsed('Time') ) { # Modification time defaults to now. $got->value('Time' => time) ; } if (! $got->parsed('exTime') ) { my $timeRef = $got->value('exTime'); if ( defined $timeRef) { return $self->saveErrorString(undef, "exTime not a 3-element array ref") if ref $timeRef ne 'ARRAY' || @$timeRef != 3; } $got->value("MTime", $timeRef->[1]); $got->value("ATime", $timeRef->[0]); $got->value("CTime", $timeRef->[2]); } *$self->{ZipData}{Zip64} = $got->value('Zip64'); *$self->{ZipData}{Stream} = $got->value('Stream'); return $self->saveErrorString(undef, "Zip64 only supported if Stream enabled") if *$self->{ZipData}{Zip64} && ! *$self->{ZipData}{Stream} ; my $method = $got->value('Method'); return $self->saveErrorString(undef, "Unknown Method '$method'") if ! defined $ZIP_CM_MIN_VERSIONS{$method}; return $self->saveErrorString(undef, "Bzip2 not available") if $method == ZIP_CM_BZIP2 and ! defined $IO::Compress::Adapter::Bzip2::VERSION; *$self->{ZipData}{Method} = $method; *$self->{ZipData}{ZipComment} = $got->value('ZipComment') ; for my $name (qw( ExtraFieldLocal ExtraFieldCentral )) { my $data = $got->value($name) ; if (defined $data) { my $bad = IO::Compress::Zlib::Extra::parseExtraField($data, 1, 0) ; return $self->saveErrorString(undef, "Error with $name Parameter: $bad") if $bad ; $got->value($name, $data) ; } } return undef if defined $IO::Compress::Bzip2::VERSION and ! IO::Compress::Bzip2::ckParams($self, $got); return 1 ; } #sub newHeader #{ # my $self = shift ; # # return $self->mkHeader(*$self->{Got}); #} sub getExtraParams { my $self = shift ; use IO::Compress::Base::Common 2.008 qw(:Parse); use Compress::Raw::Zlib 2.008 qw(Z_DEFLATED Z_DEFAULT_COMPRESSION Z_DEFAULT_STRATEGY); my @Bzip2 = (); @Bzip2 = IO::Compress::Bzip2::getExtraParams($self) if defined $IO::Compress::Bzip2::VERSION; return ( # zlib behaviour $self->getZlibParams(), 'Stream' => [1, 1, Parse_boolean, 1], #'Store' => [0, 1, Parse_boolean, 0], 'Method' => [0, 1, Parse_unsigned, ZIP_CM_DEFLATE], # # Zip header fields 'Minimal' => [0, 1, Parse_boolean, 0], 'Zip64' => [0, 1, Parse_boolean, 0], 'Comment' => [0, 1, Parse_any, ''], 'ZipComment'=> [0, 1, Parse_any, ''], 'Name' => [0, 1, Parse_any, ''], 'Time' => [0, 1, Parse_any, undef], 'exTime' => [0, 1, Parse_any, undef], 'ExtAttr' => [0, 1, Parse_any, 0], 'OS_Code' => [0, 1, Parse_unsigned, $Compress::Raw::Zlib::gzip_os_code], 'TextFlag' => [0, 1, Parse_boolean, 0], 'ExtraFieldLocal' => [0, 1, Parse_any, undef], 'ExtraFieldCentral'=> [0, 1, Parse_any, undef], @Bzip2, ); } sub getInverseClass { return ('IO::Uncompress::Unzip', \$IO::Uncompress::Unzip::UnzipError); } sub getFileInfo { my $self = shift ; my $params = shift; my $filename = shift ; my ($mode, $uid, $gid, $atime, $mtime, $ctime) = (stat($filename))[2, 4,5, 8,9,10] ; $params->value('Name' => $filename) if ! $params->parsed('Name') ; $params->value('Time' => $mtime) if ! $params->parsed('Time') ; if ( ! $params->parsed('exTime')) { $params->value('MTime' => $mtime) ; $params->value('ATime' => $atime) ; $params->value('CTime' => undef) ; # No Creation time } $params->value('ExtAttr' => $mode << 16) if ! $params->parsed('ExtAttr'); $params->value('UID' => $uid) ; $params->value('GID' => $gid) ; } sub mkExtendedTime { # order expected is m, a, c my $times = ''; my $bit = 1 ; my $flags = 0; for my $time (@_) { if (defined $time) { $flags |= $bit; $times .= pack("V", $time); } $bit <<= 1 ; } return IO::Compress::Zlib::Extra::mkSubField(ZIP_EXTRA_ID_EXT_TIMESTAMP, pack("C", $flags) . $times); } sub mkUnix2Extra { my $ids = ''; for my $id (@_) { $ids .= pack("v", $id); } return IO::Compress::Zlib::Extra::mkSubField(ZIP_EXTRA_ID_INFO_ZIP_UNIX2, $ids); } # from Archive::Zip sub _unixToDosTime # Archive::Zip::Member { my $time_t = shift; # TODO - add something to cope with unix time < 1980 my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime($time_t); my $dt = 0; $dt += ( $sec >> 1 ); $dt += ( $min << 5 ); $dt += ( $hour << 11 ); $dt += ( $mday << 16 ); $dt += ( ( $mon + 1 ) << 21 ); $dt += ( ( $year - 80 ) << 25 ); return $dt; } 1; __END__ =head1 NAME IO::Compress::Zip - Write zip files/buffers =head1 SYNOPSIS use IO::Compress::Zip qw(zip $ZipError) ; my $status = zip $input => $output [,OPTS] or die "zip failed: $ZipError\n"; my $z = new IO::Compress::Zip $output [,OPTS] or die "zip failed: $ZipError\n"; $z->print($string); $z->printf($format, $string); $z->write($string); $z->syswrite($string [, $length, $offset]); $z->flush(); $z->tell(); $z->eof(); $z->seek($position, $whence); $z->binmode(); $z->fileno(); $z->opened(); $z->autoflush(); $z->input_line_number(); $z->newStream( [OPTS] ); $z->deflateParams(); $z->close() ; $ZipError ; # IO::File mode print $z $string; printf $z $format, $string; tell $z eof $z seek $z, $position, $whence binmode $z fileno $z close $z ; =head1 DESCRIPTION This module provides a Perl interface that allows writing zip compressed data to files or buffer. The primary purpose of this module is to provide streaming write access to zip files and buffers. It is not a general-purpose file archiver. If that is what you want, check out C. At present three compression methods are supported by IO::Compress::Zip, namely Store (no compression at all), Deflate and Bzip2. Note that to create Bzip2 content, the module C must be installed. For reading zip files/buffers, see the companion module L. =head1 Functional Interface A top-level function, C, is provided to carry out "one-shot" compression between buffers and/or files. For finer control over the compression process, see the L section. use IO::Compress::Zip qw(zip $ZipError) ; zip $input => $output [,OPTS] or die "zip failed: $ZipError\n"; The functional interface needs Perl5.005 or better. =head2 zip $input => $output [, OPTS] C expects at least two parameters, C<$input> and C<$output>. =head3 The C<$input> parameter The parameter, C<$input>, is used to define the source of the uncompressed data. It can take one of the following forms: =over 5 =item A filename If the C<$input> parameter is a simple scalar, it is assumed to be a filename. This file will be opened for reading and the input data will be read from it. =item A filehandle If the C<$input> parameter is a filehandle, the input data will be read from it. The string '-' can be used as an alias for standard input. =item A scalar reference If C<$input> is a scalar reference, the input data will be read from C<$$input>. =item An array reference If C<$input> is an array reference, each element in the array must be a filename. The input data will be read from each file in turn. The complete array will be walked to ensure that it only contains valid filenames before any data is compressed. =item An Input FileGlob string If C<$input> is a string that is delimited by the characters "<" and ">" C will assume that it is an I. The input is the list of files that match the fileglob. If the fileglob does not match any files ... See L for more details. =back If the C<$input> parameter is any other type, C will be returned. In addition, if C<$input> is a simple filename, the default values for the C, C