blob: e1ee7e6c41d983226e2b899b5ab63806031ae293 [file] [log] [blame]
<?php
/**
* File containing the ezcArchiveLocalFileHeader class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Archive
* @version //autogentag//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @access private
*/
/**
* The ezcArchiveLocalFileHeader class represents the Zip local header.
*
* ezcArchiveLocalFileHeader can read the header from an ezcArchiveCharacterFile or ezcArchiveEntry.
*
* The values from the headers are directly accessible via the class properties, and allows
* reading and writing to specific header values.
*
* The entire header can be appended to an ezcArchiveCharacterFile again or written to an ezcArchiveFileStructure.
* Information may get lost, though.
*
* The overall .ZIP file format[1] :
*
* <pre>
* [local file header 1]
* [file data 1]
* [data descriptor 1]
* .
* .
* [local file header n]
* [file data n]
* [data descriptor n]
* [central directory]
* [end of central directory record]
* </pre>
*
* The Local File Header has the following structure:
*
* <pre>
* + ---+--------+------------+-------------------+-------------------------------+
* | ID | Offset | Field size | Property | Description |
* +----+--------+------------+-------------------+-------------------------------+
* | | 0 | 4 | - | Local file header signature |
* | | 4 | 2 | version | Version needed to extract |
* | | 6 | 2 | bitFlag | General purpose bit flag |
* | | 8 | 2 | compressionMethod | Compression method |
* | | 10 | 2 | lastModFileTime | Last modification file time |
* | | 12 | 2 | lastModFileDate | Last modification file date |
* | | 14 | 4 | crc | crc-32 |
* | | 18 | 4 | compressedSize | compressed size |
* | | 22 | 4 | uncompressedSize | uncompressed size |
* | X | 26 | 2 | fileNameLength | file name length |
* | Y | 28 | 2 | extraFieldLength | extra field length |
* | | 30 | X | fileName | file name |
* | | 30+X | Y | - | extra field |
* +----+--------+------------+-------------------+-------------------------------+
* </pre>
*
* The columns of the table are:
* - ID gives a label to a specific field or row in the table.
* - Offset describes the start position of a header field.
* - Field size describes the size of the field in bytes.
* - Property is the name of the property that will be set by the header field.
* - Description explains what this field describes.
*
* The local file signature cannot be changed and is set in the constant {@link self::magic}.
*
* The extra fields that are implemented are:
* - Info-Zip Unix field (old).
* - Info-Zip Unix field (new).
* - Universal timestamp.
*
* Info-Zip Unix field (old):
* <pre>
* +--------+------------+------------------------------------+
* | Offset | Field size | Description |
* +--------+------------+------------------------------------+
* | 0 | 2 | Header ID (0x5855) |
* | 2 | 2 | Data size: Full size = 12 |
* | 4 | 4 | Last Access Time |
* | 8 | 4 | Last Modification Time |
* | 12 | 2 | User ID (Optional if size < 12 ) |
* | 14 | 2 | Group ID (Optional if size < 12 ) |
* +--------+------------+------------------------------------+
* </pre>
*
* Info-Zip Unix field (new):
* <pre>
* +--------+------------+--------------------+
* | Offset | Field size | Description |
* +--------+------------+--------------------+
* | 0 | 2 | Header ID (0x7855) |
* | 2 | 2 | Data size. |
* | 4 | 2 | User ID |
* | 6 | 2 | Group ID |
* +--------+------------+--------------------+
* </pre>
*
* Universal timestamp:
* <pre>
* +--------+------------+-------------------------+
* | Offset | Field size | Description |
* +--------+------------+-------------------------+
* | 0 | 2 | Header ID (0x5455) |
* | 2 | 2 | Data size. |
* | 4 | 1 | Info bits (flags) |
* | 5 | 4 | Last Modification Time |
* | 9 | 4 | Last Access Time |
* | 13 | 4 | Creation Time |
* +--------+------------+-------------------------+
* </pre>
*
* Info bits indicate which fields are set:
* - bit 0, if set then the modification time is present.
* - bit 1, if set then the access time is present.
* - bit 2, if set then the creation time is present.
*
* These info bits are ONLY valid for the local file header. The
* central header has the same Universal Timestamp, but reflects
* the bits from the local file header!
*
*
* See PKWare documentation and InfoZip extra field documentation for more information.
*
*
* [1] PKWARE .ZIP file format specification:
* <pre>
* File: APPNOTE.TXT - .ZIP File Format Specification
* Version: 4.5
* Revised: 11/01/2001
* Copyright (c) 1989 - 2001 PKWARE Inc., All Rights Reserved.
* </pre>
*
* [2] InfoZip distribution contains the file: proginfo/extra.fld
*
* @package Archive
* @version //autogentag//
* @access private
*/
class ezcArchiveLocalFileHeader
{
/**
* Defines the signature of this header.
*/
const magic = 0x04034b50;
/**
* Extra field definition.
*
* UNIX Extra Field ID ("UX").
*/
const EF_IZUNIX = 0x5855;
/**
* Extra field definition.
*
* Info-ZIP's new Unix( "Ux" ).
*/
const EF_IZUNIX2 = 0x7855;
/**
* Extra field definition.
*
* Universal timestamp( "UT" ).
*/
const EF_TIME = 0x5455;
/**
* Minimal UT field contains Flags byte
*/
const EB_UT_MINLEN = 1;
/**
* Byte offset of Flags field
*/
const EB_UT_FLAGS = 0;
/**
* Byte offset of 1st time value
*/
const EB_UT_TIME1 = 1;
/**
* mtime present
*/
const EB_UT_FL_MTIME = 1;
/**
* atime present
*/
const EB_UT_FL_ATIME = 2;
/**
* ctime present
*/
const EB_UT_FL_CTIME = 4;
/**
* UT field length.
*/
const EB_UT_FL_LEN = 4;
/**
* Minimal "UX" field contains atime, mtime
*/
const EB_UX_MINLEN = 8;
/**
* Offset of atime in "UX" extra field data
*/
const EB_UX_ATIME = 0;
/**
* Offset of mtime in "UX" extra field data
*/
const EB_UX_MTIME = 4;
/**
* Full "UX" field ( atime, mtime, uid, gid )
*/
const EB_UX_FULLSIZE = 12;
/**
* Byte offset of UID in "UX" field data
*/
const EB_UX_UID = 8;
/**
* Byte offset of GID in "UX" field data
*/
const EB_UX_GID = 10;
/**
* Minimal Ux field contains UID/GID
*/
const EB_UX2_MINLEN = 4;
/**
* Byte offset of UID in "Ux" field data
*/
const EB_UX2_UID = 0;
/**
* Byte offset of GID in "Ux" field data
*/
const EB_UX2_GID = 2;
/**
* Byte offset of valid in "Ux" field data
*/
const EB_UX2_VALID = 256;
/**
* Holds the properties of this class.
*
* @var array(string=>mixed)
*/
protected $properties = array();
/**
* Holds the user ID and is false if not set.
*
* @var int
*/
protected $userId = false;
/**
* Holds the group ID and is false if not set.
*
* @var int
*/
protected $groupId = false;
/**
* Holds the modification time as a timestamp and is false if not set.
*
* @var int
*/
protected $mtime = false;
/**
* Holds the modification time as a timestamp and is false if not set.
*
* @var int
*/
protected $atime = false;
/**
* Holds the creation time as a timestamp and is false if not set.
*
* @var int
*/
protected $ctime = false;
/**
* Creates and initializes a new header.
*
* If the ezcArchiveCharacterFile $file is null then the header will be empty.
* When an ezcArchiveCharacterFile is given, the file position should be directly after the
* signature of the header. This header will be read from the file and initialized in this class.
*
* @param ezcArchiveCharacterFile $file
*/
public function __construct( ezcArchiveCharacterFile $file = null, ezcArchiveCentralDirectoryHeader $centralHeader = null )
{
// Offset | Field size | Description
// ----------------------------------
// 0 | 4 | Local file header signature ( 0x04034b50 ).
// 4 | 2 | Version needed to extract.
// 6 | 2 | General purpose bit flag.
// 8 | 2 | Compression method.
// 10 | 2 | Last mod file time.
// 12 | 2 | Last mod file date.
// 14 | 4 | Crc-32.
// 18 | 4 | Compressed size.
// 22 | 4 | Uncompressed size.
// 26 | 2 | File name length.
// 28 | 2 | Extra field length.
if ( !is_null( $file ) )
{
$this->properties = unpack (
"vversion/".
"vbitFlag/".
"vcompressionMethod/".
"vlastModFileTime/".
"vlastModFileDate/".
"Vcrc/".
"VcompressedSize/".
"VuncompressedSize/".
"vfileNameLength/".
"vextraFieldLength",
$file->read( 26 ) );
$this->properties["fileName"] = $file->read( $this->properties["fileNameLength"] );
$extraField = $file->read( $this->properties["extraFieldLength"] );
// Append extra field information.
$this->setExtraFieldData( $extraField );
// Fix the local file headers from the central header, in case crc,
// compressed size and original size are not set. This is needed
// for Open Office documents at least.
if ( $centralHeader && $this->properties['compressedSize'] === 0 && $this->properties['uncompressedSize'] === 0 )
{
$this->properties['compressedSize'] = $centralHeader->compressedSize;
$this->properties['uncompressedSize'] = $centralHeader->uncompressedSize;
$this->properties['crc'] = $centralHeader->crc;
}
// Skip the file.
$file->seek( $this->compressedSize, SEEK_CUR );
}
}
/**
* Serializes this header and appends it to the given ezcArchiveCharacterFile $archiveFile.
*
* @param ezcArchiveCharacterFile $archiveFile
* @return void
*/
public function writeEncodedHeader( $archiveFile )
{
$this->properties["extraFieldLength" ] = 21; // FIXME.
$enc = pack( "VvvvvvVVVvv",
self::magic,
$this->properties["version"],
$this->properties["bitFlag"],
$this->properties["compressionMethod"],
$this->properties["lastModFileTime"],
$this->properties["lastModFileDate"],
$this->properties["crc"],
$this->properties["compressedSize"],
$this->properties["uncompressedSize"],
$this->properties["fileNameLength"],
$this->properties["extraFieldLength"] );
$time = pack( "vvcVV", self::EF_TIME, 9, 3, $this->mtime, $this->atime ); // set accesss time to modification time.
$unix2 = pack( "vvvv", self::EF_IZUNIX2, 4, $this->userId, $this->groupId );
$archiveFile->write( $enc . $this->fileName . $time . $unix2 );
}
/**
* Sets the property $name to $value.
*
* @throws ezcBasePropertyNotFoundException if the property does not exist
* @throws ezcBasePropertyReadOnlyException if the property is read-only
* @param string $name
* @param mixed $value
* @return void
* @ignore
*/
public function __set( $name, $value )
{
switch ( $name )
{
case "version":
case "bitFlag":
case "compressionMethod":
case "crc":
case "compressedSize":
case "uncompressedSize":
case "extraFieldLength":
$this->properties[$name] = $value;
break;
case "lastModFileTime":
case "lastModFileDate":
case "fileNameLength":
throw new ezcBasePropertyReadOnlyException( $name );
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Returns the value of the property $name.
*
* @throws ezcBasePropertyNotFoundException if the property does not exist.
* @param string $name
* @return mixed
* @ignore
*/
public function __get( $name )
{
switch ( $name )
{
case "version":
case "bitFlag":
case "compressionMethod":
case "lastModFileTime":
case "lastModFileDate":
case "crc":
case "compressedSize":
case "uncompressedSize":
case "extraFieldLength":
case "fileNameLength":
case "fileName":
return $this->properties[ $name ];
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Returns the modification timestamp.
*
* The timestamp from the extra fields is returned if it is available.
* If it cannot be found, the ZIP time is converted to a timestamp.
*
* @return int
*/
public function getModificationTime()
{
return ( $this->mtime ? $this->mtime : self::dosFormatToTimestamp( array( $this->lastModFileTime, $this->lastModFileDate ) ) );
}
/**
* Sets the modification time to the int $timestamp.
*
* The properties lastModFileTime and lastModFileDate properties will be set.
*
* @param int $timestamp
* @return void
*/
public function setModificationTime( $timestamp )
{
$this->mtime = $timestamp;
list( $this->properties["lastModFileTime"], $this->properties["lastModFileDate"] ) = self::timestampToDosFormat( $timestamp );
}
/**
* Returns an two element array with, respectively, the dos time and dos date, which are converted from
* the given int $timestamp.
*
* The DOS time and date format are described here: {@link http://www.vsft.com/hal/dostime.htm}.
*
* @see http://www.vsft.com/hal/dostime.htm
*
* @param int $timestamp
* @return array(int)
*/
public static function timestampToDosFormat( $timestamp )
{
$time = getdate( $timestamp );
$dosTime = ( $time["hours"] & 31 ); // Hours. Bit: 11 .. 15
$dosTime <<= 6;
$dosTime |= ( $time["minutes"] & 63 ); // Minutes.
$dosTime <<= 5;
$dosTime |= ( ( int ) ( $time["seconds"] / 2 ) & 31 ); // seconds.
$dosDate = ( $time["year"] - 1980 ) & 127; // Year.
$dosDate <<= 4;
$dosDate |= ( $time["mon"] & 15 ); // month.
$dosDate <<= 5;
$dosDate |= ( $time["mday"] & 31 ); // day.
return array( $dosTime, $dosDate );
}
/**
* Returns the timestamp which is converted from an array with, respectively, the dos time and dos date.
*
* Expects an array with two elements.
*
* The DOS time and date format are described here: {@link http://www.vsft.com/hal/dostime.htm}.
*
* @see http://www.vsft.com/hal/dostime.htm
*
* @param array(int) $array
* @return int
*/
public static function dosFormatToTimestamp( array $array )
{
$dosTime = $array[0];
$seconds = ( ($dosTime ) & 31 ) * 2 ; // Bit 0 .. 4
$minutes = ( $dosTime >> 5 ) & 63 ; // Bit 5 .. 10
$hours = ( $dosTime >> 11 ) & 31; // Bit 11 .. 15
$dosDate = $array[1];
$day = ( ( $dosDate ) & 31 ); // Bit 0 .. 4
$month = ( $dosDate >> 5 ) & 15 ; // Bit 5 .. 8
$year = ( ( $dosDate >> 9 ) & 127 ) + 1980; // Bit 9 .. 15
return mktime( $hours, $minutes, $seconds, $month, $day, $year );
}
/**
* Sets the filename in the header.
*
* The properties fileName and file length are changed.
*
* @param string $name
*/
public function setFileName( $name )
{
$this->properties["fileName"] = $name;
$this->properties["fileNameLength"] = strlen( $name );
}
/**
* Sets the compression.
*
* @param int $compressionMethod
* @param int $compressedSize
* @return void
*/
public function setCompression( $compressionMethod, $compressedSize )
{
// Fixme, for now only decompressed.
$this->compressionMethod = $compressionMethod;
$this->compressedSize = $compressedSize;
}
/**
* Updates the given ezcArchiveFileStructure $struct with the values from this header.
*
* If bool $override is false, this method will not overwrite the values from the ezcArchiveFileStructure $struct.
* The values that can be set in the archiveFileStructure are: path, gid, uid, mtime, and size.
*
* @param ezcArchiveFileStructure &$struct
* @param bool $override
* @return void
*/
public function setArchiveFileStructure( ezcArchiveFileStructure &$struct, $override = false )
{
if ( !isset( $struct->path ) || $override )
{
$struct->path = $this->fileName;
}
if ( !isset( $struct->size ) || $override )
{
$struct->size = $this->uncompressedSize;
}
if ( $this->groupId && ( !isset( $struct->size ) || $override ) )
{
$struct->gid = $this->groupId;
}
if ( $this->userId && ( !isset( $struct->size ) || $override ) )
{
$struct->uid = $this->userId;
}
if ( $this->mtime && ( !isset( $struct->mtime ) || $override ) )
{
$struct->mtime = $this->mtime;
}
if ( $this->atime && ( !isset( $struct->atime ) || $override ) )
{
$struct->atime = $this->atime;
}
}
/**
* Sets this header with the values from the ezcArchiveEntry $entry.
*
* The values that are possible to set from the ezcArchiveEntry $entry are set in this header.
*
* @param ezcArchiveEntry $entry
* @return void
*/
public function setHeaderFromArchiveEntry( ezcArchiveEntry $entry )
{
$this->version = 10; // FIXME
$this->bitFlag = 0; // FIXME
$this->atime = $entry->getAccessTime();
$this->groupId = $entry->getGroupId();
$this->userId = $entry->getUserId();
$this->setModificationTime( $entry->getModificationTime() );
if ( $entry->isSymLink() )
{
$this->uncompressedSize = strlen( $entry->getLink() );
$this->crc = ezcArchiveChecksums::getCrc32FromString( $entry->getLink() );
}
else
{
$this->uncompressedSize = $entry->getSize();
$this->crc = ezcArchiveChecksums::getCrc32FromFile( $entry->getPath() );
}
$this->setFileName( $entry->getPath( false ) );
}
/**
* Returns the total size of this header.
*
* @return int
*/
public function getHeaderSize()
{
return 30 + $this->properties["fileNameLength"] + $this->properties["extraFieldLength"];
}
/**
* Decodes and sets the extra header properties from the string $data.
*
* @param string $data
* @return void
*/
protected function setExtraFieldData( $data )
{
$raw = array();
$offset = 0;
$dataLength = strlen( $data );
while ( $offset < $dataLength )
{
// Read header.
$dec = unpack( "vid/vlength", substr( $data, $offset, 4 ) );
$offset += 4;
switch ( $dec["id"] )
{
case self::EF_IZUNIX2:
$raw[ "EF_IZUNIX2" ] = $this->getNewInfoZipExtraField( substr( $data, $offset, $dec["length"] ), $dec["length"] );
break;
case self::EF_IZUNIX:
$raw[ "EF_IZUNIX" ] = $this->getOldInfoZipExtraField( substr( $data, $offset, $dec["length"] ), $dec["length"] );
break;
case self::EF_TIME:
$raw[ "EF_TIME" ] = $this->getUniversalTimestampField( substr( $data, $offset, $dec["length"] ), $dec["length"] );
break;
}
$offset += $dec["length"];
}
$result = array();
// The order is important.
if ( isset( $raw["EF_TIME"]["mtime"] ) )
{
$this->mtime = $raw["EF_TIME"]["mtime"];
}
if ( isset( $raw["EF_TIME"]["atime"] ) )
{
$this->atime = $raw["EF_TIME"]["atime"];
}
if ( isset( $raw["EF_TIME"]["ctime"] ) )
{
$this->ctime = $raw["EF_TIME"]["ctime"];
}
if ( isset( $raw["EF_IZUNIX"]["mtime"] ) )
{
$this->mtime = $raw["EF_IZUNIX"]["mtime"];
}
if ( isset( $raw["EF_IZUNIX"]["atime"] ) )
{
$this->atime = $raw["EF_IZUNIX"]["atime"];
}
if ( isset( $raw["EF_IZUNIX"]["gid"] ) )
{
$this->groupId = $raw["EF_IZUNIX"]["gid"];
}
if ( isset( $raw["EF_IZUNIX"]["uid"] ) )
{
$this->userId = $raw["EF_IZUNIX"]["uid"];
}
if ( isset( $raw["EF_IZUNIX2"]["gid"] ) )
{
$this->groupId = $raw["EF_IZUNIX2"]["gid"];
}
if ( isset( $raw["EF_IZUNIX2"]["uid"] ) )
{
$this->userId = $raw["EF_IZUNIX2"]["uid"];
}
return $result;
}
/**
* Returns an array with the decoded 'new Info-Zip' data.
*
* @param string $field
* @param int $length
* @return array(string)
*/
protected function getNewInfoZipExtraField( $field, $length )
{
$localOffset = 0;
if ( $length >= self::EB_UX2_MINLEN )
{
return unpack( "vuid/vgid", substr( $field, 0, self::EB_UX2_MINLEN ) );
}
return array();
}
/**
* Returns an array with the decoded 'old Info-Zip' data.
*
* @param string $field
* @param int $length
* @return array(string)
*/
protected function getOldInfoZipExtraField( $field, $length )
{
if ( $length >= self::EB_UX_FULLSIZE )
{
return unpack( "Vatime/Vmtime/vuid/vgid", substr( $field, 0, self::EB_UX_FULLSIZE ) );
}
else if ( $length >= self::EB_UX_MINLEN )
{
return unpack( "Vatime/Vmtime", substr( $field, 0, self::EB_UX_MINLEN ) );
}
return array();
}
/**
* Returns an array with the decoded 'universal timestamp' data.
*
* @param string $field
* @param int $length
* @return array(string)
*/
protected function getUniversalTimestampField( $field, $length )
{
$localOffset = 0;
$result = array();
if ( $length >= self::EB_UT_MINLEN )
{
$f = unpack( "cflags", substr( $field, $localOffset, 1 ) );
$length--; // Flag takes one byte.
if ( ( $f["flags"] & self::EB_UT_FL_MTIME ) && $length >= 4 )
{
$result = array_merge( $result, unpack( "Vmtime", substr( $field, $localOffset + self::EB_UT_MINLEN + ( self::EB_UT_FL_LEN * 0 ), self::EB_UT_FL_LEN ) ) );
$length -= 4;
}
if ( $f["flags"] & self::EB_UT_FL_ATIME && $length >= 4 )
{
$result = array_merge( $result, unpack( "Vatime", substr( $field, $localOffset + self::EB_UT_MINLEN + ( self::EB_UT_FL_LEN * 1 ), self::EB_UT_FL_LEN ) ) );
$length -= 4;
}
if ( $f["flags"] & self::EB_UT_FL_CTIME && $length >= 4 )
{
$result = array_merge( $result, unpack( "Vctime", substr( $field, $localOffset + self::EB_UT_MINLEN + ( self::EB_UT_FL_LEN * 2 ), self::EB_UT_FL_LEN ) ) );
$length -= 4;
}
}
return $result;
}
/**
* Returns true if the given string $string matches with the current signature.
*
* @param string $string
* @return bool
*/
public static function isSignature( $string )
{
return $string == pack( "V", self::magic );
}
}
?>