init commit

This commit is contained in:
2026-03-17 09:56:00 +08:00
commit e2c8ae752d
6827 changed files with 1211784 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra;
/**
* Represents a collection of Extra Fields as they may
* be present at several locations in ZIP files.
*/
class ExtraFieldsCollection implements \ArrayAccess, \Countable, \Iterator
{
/**
* The map of Extra Fields.
* Maps from Header ID to Extra Field.
* Must not be null, but may be empty if no Extra Fields are used.
* The map is sorted by Header IDs in ascending order.
*
* @var ZipExtraField[]
*/
protected array $collection = [];
/**
* Returns the number of Extra Fields in this collection.
*/
public function count(): int
{
return \count($this->collection);
}
/**
* Returns the Extra Field with the given Header ID or null
* if no such Extra Field exists.
*
* @param int $headerId the requested Header ID
*
* @return ZipExtraField|null the Extra Field with the given Header ID or
* if no such Extra Field exists
*/
public function get(int $headerId): ?ZipExtraField
{
$this->validateHeaderId($headerId);
return $this->collection[$headerId] ?? null;
}
private function validateHeaderId(int $headerId): void
{
if ($headerId < 0 || $headerId > 0xFFFF) {
throw new \InvalidArgumentException('$headerId out of range');
}
}
/**
* Stores the given Extra Field in this collection.
*
* @param ZipExtraField $extraField the Extra Field to store in this collection
*
* @return ZipExtraField the Extra Field previously associated with the Header ID of
* of the given Extra Field or null if no such Extra Field existed
*/
public function add(ZipExtraField $extraField): ZipExtraField
{
$headerId = $extraField->getHeaderId();
$this->validateHeaderId($headerId);
$this->collection[$headerId] = $extraField;
return $extraField;
}
/**
* @param ZipExtraField[] $extraFields
*/
public function addAll(array $extraFields): void
{
foreach ($extraFields as $extraField) {
$this->add($extraField);
}
}
/**
* @param ExtraFieldsCollection $collection
*/
public function addCollection(self $collection): void
{
$this->addAll($collection->collection);
}
/**
* @return ZipExtraField[]
*/
public function getAll(): array
{
return $this->collection;
}
/**
* Returns Extra Field exists.
*
* @param int $headerId the requested Header ID
*/
public function has(int $headerId): bool
{
return isset($this->collection[$headerId]);
}
/**
* Removes the Extra Field with the given Header ID.
*
* @param int $headerId the requested Header ID
*
* @return ZipExtraField|null the Extra Field with the given Header ID or null
* if no such Extra Field exists
*/
public function remove(int $headerId): ?ZipExtraField
{
$this->validateHeaderId($headerId);
if (isset($this->collection[$headerId])) {
$ef = $this->collection[$headerId];
unset($this->collection[$headerId]);
return $ef;
}
return null;
}
/**
* Whether a offset exists.
*
* @see http://php.net/manual/en/arrayaccess.offsetexists.php
*
* @param mixed $offset an offset to check for
*
* @return bool true on success or false on failure
*/
public function offsetExists($offset): bool
{
return isset($this->collection[(int) $offset]);
}
/**
* Offset to retrieve.
*
* @see http://php.net/manual/en/arrayaccess.offsetget.php
*
* @param mixed $offset the offset to retrieve
*/
public function offsetGet($offset): ?ZipExtraField
{
return $this->collection[(int) $offset] ?? null;
}
/**
* Offset to set.
*
* @see http://php.net/manual/en/arrayaccess.offsetset.php
*
* @param mixed $offset the offset to assign the value to
* @param mixed $value the value to set
*/
public function offsetSet($offset, $value): void
{
if (!$value instanceof ZipExtraField) {
throw new \InvalidArgumentException('value is not instanceof ' . ZipExtraField::class);
}
$this->add($value);
}
/**
* Offset to unset.
*
* @see http://php.net/manual/en/arrayaccess.offsetunset.php
*
* @param mixed $offset the offset to unset
*/
public function offsetUnset($offset): void
{
$this->remove($offset);
}
/**
* Return the current element.
*
* @see http://php.net/manual/en/iterator.current.php
*/
public function current(): ZipExtraField
{
return current($this->collection);
}
/**
* Move forward to next element.
*
* @see http://php.net/manual/en/iterator.next.php
*/
public function next(): void
{
next($this->collection);
}
/**
* Return the key of the current element.
*
* @see http://php.net/manual/en/iterator.key.php
*
* @return int scalar on success, or null on failure
*/
public function key(): int
{
return key($this->collection);
}
/**
* Checks if current position is valid.
*
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
*/
public function valid(): bool
{
return key($this->collection) !== null;
}
/**
* Rewind the Iterator to the first element.
*
* @see http://php.net/manual/en/iterator.rewind.php
*/
public function rewind(): void
{
reset($this->collection);
}
public function clear(): void
{
$this->collection = [];
}
public function __toString(): string
{
$formats = [];
foreach ($this->collection as $key => $value) {
$formats[] = (string) $value;
}
return implode("\n", $formats);
}
/**
* If clone extra fields.
*/
public function __clone()
{
foreach ($this->collection as $k => $v) {
$this->collection[$k] = clone $v;
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* A common base class for Unicode extra information extra fields.
*/
abstract class AbstractUnicodeExtraField implements ZipExtraField
{
public const DEFAULT_VERSION = 0x01;
private int $crc32;
private string $unicodeValue;
public function __construct(int $crc32, string $unicodeValue)
{
$this->crc32 = $crc32;
$this->unicodeValue = $unicodeValue;
}
/**
* @return int the CRC32 checksum of the filename or comment as
* encoded in the central directory of the zip file
*/
public function getCrc32(): int
{
return $this->crc32;
}
public function setCrc32(int $crc32): void
{
$this->crc32 = $crc32;
}
public function getUnicodeValue(): string
{
return $this->unicodeValue;
}
/**
* @param string $unicodeValue the UTF-8 encoded name to set
*/
public function setUnicodeValue(string $unicodeValue): void
{
$this->unicodeValue = $unicodeValue;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException on error
*
* @return static
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
if (\strlen($buffer) < 5) {
throw new ZipException('Unicode path extra data must have at least 5 bytes.');
}
[
'version' => $version,
'crc32' => $crc32,
] = unpack('Cversion/Vcrc32', $buffer);
if ($version !== self::DEFAULT_VERSION) {
throw new ZipException(sprintf('Unsupported version [%d] for Unicode path extra data.', $version));
}
$unicodeValue = substr($buffer, 5);
return new static($crc32, $unicodeValue);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException on error
*
* @return static
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
return pack(
'CV',
self::DEFAULT_VERSION,
$this->crc32
)
. $this->unicodeValue;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
return $this->packLocalFileData();
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Apk Alignment Extra Field.
*
* @see https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/ApkSigner.java
* @see https://developer.android.com/studio/command-line/zipalign
*/
final class ApkAlignmentExtraField implements ZipExtraField
{
/**
* @var int Extensible data block/field header ID used for storing
* information about alignment of uncompressed entries as
* well as for aligning the entries's data. See ZIP
* appnote.txt section 4.5 Extensible data fields.
*/
public const HEADER_ID = 0xD935;
/** @var int */
public const ALIGNMENT_BYTES = 4;
/** @var int */
public const COMMON_PAGE_ALIGNMENT_BYTES = 4096;
private int $multiple;
private int $padding;
public function __construct(int $multiple, int $padding)
{
$this->multiple = $multiple;
$this->padding = $padding;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
public function getMultiple(): int
{
return $this->multiple;
}
public function getPadding(): int
{
return $this->padding;
}
public function setMultiple(int $multiple): void
{
$this->multiple = $multiple;
}
public function setPadding(int $padding): void
{
$this->padding = $padding;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException
*
* @return ApkAlignmentExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
$length = \strlen($buffer);
if ($length < 2) {
// This is APK alignment field.
// FORMAT:
// * uint16 alignment multiple (in bytes)
// * remaining bytes -- padding to achieve alignment of data which starts after
// the extra field
throw new ZipException(
'Minimum 6 bytes of the extensible data block/field used for alignment of uncompressed entries.'
);
}
$multiple = unpack('v', $buffer)[1];
$padding = $length - 2;
return new self($multiple, $padding);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException on error
*
* @return ApkAlignmentExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
return pack('vx' . $this->padding, $this->multiple);
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
return $this->packLocalFileData();
}
public function __toString(): string
{
return sprintf(
'0x%04x APK Alignment: Multiple=%d Padding=%d',
self::HEADER_ID,
$this->multiple,
$this->padding
);
}
}

View File

@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Constants\UnixStat;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* ASi Unix Extra Field:
* ====================.
*
* The following is the layout of the ASi extra block for Unix. The
* local-header and central-header versions are identical.
* (Last Revision 19960916)
*
* Value Size Description
* ----- ---- -----------
* (Unix3) 0x756e Short tag for this extra block type ("nu")
* TSize Short total data size for this block
* CRC Long CRC-32 of the remaining data
* Mode Short file permissions
* SizDev Long symlink'd size OR major/minor dev num
* UID Short user ID
* GID Short group ID
* (var.) variable symbolic link filename
*
* Mode is the standard Unix st_mode field from struct stat, containing
* user/group/other permissions, setuid/setgid and symlink info, etc.
*
* If Mode indicates that this file is a symbolic link, SizDev is the
* size of the file to which the link points. Otherwise, if the file
* is a device, SizDev contains the standard Unix st_rdev field from
* struct stat (includes the major and minor numbers of the device).
* SizDev is undefined in other cases.
*
* If Mode indicates that the file is a symbolic link, the final field
* will be the name of the file to which the link points. The file-
* name length can be inferred from TSize.
*
* [Note that TSize may incorrectly refer to the data size not counting
* the CRC; i.e., it may be four bytes too small.]
*
* @see ftp://ftp.info-zip.org/pub/infozip/doc/appnote-iz-latest.zip Info-ZIP version Specification
*/
final class AsiExtraField implements ZipExtraField
{
/** @var int Header id */
public const HEADER_ID = 0x756E;
public const USER_GID_PID = 1000;
/** Bits used for permissions (and sticky bit). */
public const PERM_MASK = 07777;
/** @var int Standard Unix stat(2) file mode. */
private int $mode;
/** @var int User ID. */
private int $uid;
/** @var int Group ID. */
private int $gid;
/**
* @var string File this entry points to, if it is a symbolic link.
* Empty string - if entry is not a symbolic link.
*/
private string $link;
public function __construct(int $mode, int $uid = self::USER_GID_PID, int $gid = self::USER_GID_PID, string $link = '')
{
$this->mode = $mode;
$this->uid = $uid;
$this->gid = $gid;
$this->link = $link;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws Crc32Exception
*
* @return AsiExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
$givenChecksum = unpack('V', $buffer)[1];
$buffer = substr($buffer, 4);
$realChecksum = crc32($buffer);
if ($givenChecksum !== $realChecksum) {
throw new Crc32Exception('Asi Unix Extra Filed Data', $givenChecksum, $realChecksum);
}
[
'mode' => $mode,
'linkSize' => $linkSize,
'uid' => $uid,
'gid' => $gid,
] = unpack('vmode/VlinkSize/vuid/vgid', $buffer);
$link = '';
if ($linkSize > 0) {
$link = substr($buffer, 10);
}
return new self($mode, $uid, $gid, $link);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws Crc32Exception
*
* @return AsiExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
$data = pack(
'vVvv',
$this->mode,
\strlen($this->link),
$this->uid,
$this->gid
) . $this->link;
return pack('V', crc32($data)) . $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
return $this->packLocalFileData();
}
/**
* Name of linked file.
*
* @return string name of the file this entry links to if it is a
* symbolic link, the empty string otherwise
*/
public function getLink(): string
{
return $this->link;
}
/**
* Indicate that this entry is a symbolic link to the given filename.
*
* @param string $link name of the file this entry links to, empty
* string if it is not a symbolic link
*/
public function setLink(string $link): void
{
$this->link = $link;
$this->mode = $this->getPermissionsMode($this->mode);
}
/**
* Is this entry a symbolic link?
*
* @return bool true if this is a symbolic link
*/
public function isLink(): bool
{
return !empty($this->link);
}
/**
* Get the file mode for given permissions with the correct file type.
*
* @param int $mode the mode
*
* @return int the type with the mode
*/
private function getPermissionsMode(int $mode): int
{
$type = 0;
if ($this->isLink()) {
$type = UnixStat::UNX_IFLNK;
} elseif (($mode & UnixStat::UNX_IFREG) !== 0) {
$type = UnixStat::UNX_IFREG;
} elseif (($mode & UnixStat::UNX_IFDIR) !== 0) {
$type = UnixStat::UNX_IFDIR;
}
return $type | ($mode & self::PERM_MASK);
}
/**
* Is this entry a directory?
*
* @return bool true if this entry is a directory
*/
public function isDirectory(): bool
{
return ($this->mode & UnixStat::UNX_IFDIR) !== 0 && !$this->isLink();
}
public function getMode(): int
{
return $this->mode;
}
public function setMode(int $mode): void
{
$this->mode = $this->getPermissionsMode($mode);
}
public function getUserId(): int
{
return $this->uid;
}
public function setUserId(int $uid): void
{
$this->uid = $uid;
}
public function getGroupId(): int
{
return $this->gid;
}
public function setGroupId(int $gid): void
{
$this->gid = $gid;
}
public function __toString(): string
{
return sprintf(
'0x%04x ASI: Mode=%o UID=%d GID=%d Link="%s',
self::HEADER_ID,
$this->mode,
$this->uid,
$this->gid,
$this->link
);
}
}

View File

@@ -0,0 +1,436 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Extended Timestamp Extra Field:
* ==============================.
*
* The following is the layout of the extended-timestamp extra block.
* (Last Revision 19970118)
*
* Local-header version:
*
* Value Size Description
* ----- ---- -----------
* (time) 0x5455 Short tag for this extra block type ("UT")
* TSize Short total data size for this block
* Flags Byte info bits
* (ModTime) Long time of last modification (UTC/GMT)
* (AcTime) Long time of last access (UTC/GMT)
* (CrTime) Long time of original creation (UTC/GMT)
*
* Central-header version:
*
* Value Size Description
* ----- ---- -----------
* (time) 0x5455 Short tag for this extra block type ("UT")
* TSize Short total data size for this block
* Flags Byte info bits (refers to local header!)
* (ModTime) Long time of last modification (UTC/GMT)
*
* The central-header extra field contains the modification time only,
* or no timestamp at all. TSize is used to flag its presence or
* absence. But note:
*
* If "Flags" indicates that Modtime is present in the local header
* field, it MUST be present in the central header field, too!
* This correspondence is required because the modification time
* value may be used to support trans-timezone freshening and
* updating operations with zip archives.
*
* The time values are in standard Unix signed-long format, indicating
* the number of seconds since 1 January 1970 00:00:00. The times
* are relative to Coordinated Universal Time (UTC), also sometimes
* referred to as Greenwich Mean Time (GMT). To convert to local time,
* the software must know the local timezone offset from UTC/GMT.
*
* The lower three bits of Flags in both headers indicate which time-
* stamps are present in the LOCAL extra field:
*
* bit 0 if set, modification time is present
* bit 1 if set, access time is present
* bit 2 if set, creation time is present
* bits 3-7 reserved for additional timestamps; not set
*
* Those times that are present will appear in the order indicated, but
* any combination of times may be omitted. (Creation time may be
* present without access time, for example.) TSize should equal
* (1 + 4*(number of set bits in Flags)), as the block is currently
* defined. Other timestamps may be added in the future.
*
* @see ftp://ftp.info-zip.org/pub/infozip/doc/appnote-iz-latest.zip Info-ZIP version Specification
*/
final class ExtendedTimestampExtraField implements ZipExtraField
{
/** @var int Header id */
public const HEADER_ID = 0x5455;
/**
* @var int the bit set inside the flags by when the last modification time
* is present in this extra field
*/
public const MODIFY_TIME_BIT = 1;
/**
* @var int the bit set inside the flags by when the last access time is
* present in this extra field
*/
public const ACCESS_TIME_BIT = 2;
/**
* @var int the bit set inside the flags by when the original creation time
* is present in this extra field
*/
public const CREATE_TIME_BIT = 4;
/**
* @var int The 3 boolean fields (below) come from this flags byte. The remaining 5 bits
* are ignored according to the current version of the spec (December 2012).
*/
private int $flags;
/** @var int|null Modify time */
private ?int $modifyTime;
/** @var int|null Access time */
private ?int $accessTime;
/** @var int|null Create time */
private ?int $createTime;
public function __construct(int $flags, ?int $modifyTime, ?int $accessTime, ?int $createTime)
{
$this->flags = $flags;
$this->modifyTime = $modifyTime;
$this->accessTime = $accessTime;
$this->createTime = $createTime;
}
/**
* @param ?int $modifyTime
* @param ?int $accessTime
* @param ?int $createTime
*
* @return ExtendedTimestampExtraField
*/
public static function create(?int $modifyTime, ?int $accessTime, ?int $createTime): self
{
$flags = 0;
if ($modifyTime !== null) {
$flags |= self::MODIFY_TIME_BIT;
}
if ($accessTime !== null) {
$flags |= self::ACCESS_TIME_BIT;
}
if ($createTime !== null) {
$flags |= self::CREATE_TIME_BIT;
}
return new self($flags, $modifyTime, $accessTime, $createTime);
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return ExtendedTimestampExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
$length = \strlen($buffer);
$flags = unpack('C', $buffer)[1];
$offset = 1;
$modifyTime = null;
$accessTime = null;
$createTime = null;
if (($flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT) {
$modifyTime = unpack('V', substr($buffer, $offset, 4))[1];
$offset += 4;
}
// Notice the extra length check in case we are parsing the shorter
// central data field (for both access and create timestamps).
if ((($flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT) && $offset + 4 <= $length) {
$accessTime = unpack('V', substr($buffer, $offset, 4))[1];
$offset += 4;
}
if ((($flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT) && $offset + 4 <= $length) {
$createTime = unpack('V', substr($buffer, $offset, 4))[1];
}
return new self($flags, $modifyTime, $accessTime, $createTime);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return ExtendedTimestampExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
$data = '';
if (($this->flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT && $this->modifyTime !== null) {
$data .= pack('V', $this->modifyTime);
}
if (($this->flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT && $this->accessTime !== null) {
$data .= pack('V', $this->accessTime);
}
if (($this->flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT && $this->createTime !== null) {
$data .= pack('V', $this->createTime);
}
return pack('C', $this->flags) . $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* Note: even if bit1 and bit2 are set, the Central data will still
* not contain access/create fields: only local data ever holds those!
*
* @return string the data
*/
public function packCentralDirData(): string
{
$cdLength = 1 + ($this->modifyTime !== null ? 4 : 0);
return substr($this->packLocalFileData(), 0, $cdLength);
}
/**
* Gets flags byte.
*
* The flags byte tells us which of the three datestamp fields are
* present in the data:
* bit0 - modify time
* bit1 - access time
* bit2 - create time
*
* Only first 3 bits of flags are used according to the
* latest version of the spec (December 2012).
*
* @return int flags byte indicating which of the
* three datestamp fields are present
*/
public function getFlags(): int
{
return $this->flags;
}
/**
* Returns the modify time (seconds since epoch) of this zip entry,
* or null if no such timestamp exists in the zip entry.
*
* @return int|null modify time (seconds since epoch) or null
*/
public function getModifyTime(): ?int
{
return $this->modifyTime;
}
/**
* Returns the access time (seconds since epoch) of this zip entry,
* or null if no such timestamp exists in the zip entry.
*
* @return int|null access time (seconds since epoch) or null
*/
public function getAccessTime(): ?int
{
return $this->accessTime;
}
/**
* Returns the create time (seconds since epoch) of this zip entry,
* or null if no such timestamp exists in the zip entry.
*
* Note: modern linux file systems (e.g., ext2)
* do not appear to store a "create time" value, and so
* it's usually omitted altogether in the zip extra
* field. Perhaps other unix systems track this.
*
* @return int|null create time (seconds since epoch) or null
*/
public function getCreateTime(): ?int
{
return $this->createTime;
}
/**
* Returns the modify time as a \DateTimeInterface
* of this zip entry, or null if no such timestamp exists in the zip entry.
* The milliseconds are always zeroed out, since the underlying data
* offers only per-second precision.
*
* @return \DateTimeInterface|null modify time as \DateTimeInterface or null
*/
public function getModifyDateTime(): ?\DateTimeInterface
{
return self::timestampToDateTime($this->modifyTime);
}
/**
* Returns the access time as a \DateTimeInterface
* of this zip entry, or null if no such timestamp exists in the zip entry.
* The milliseconds are always zeroed out, since the underlying data
* offers only per-second precision.
*
* @return \DateTimeInterface|null access time as \DateTimeInterface or null
*/
public function getAccessDateTime(): ?\DateTimeInterface
{
return self::timestampToDateTime($this->accessTime);
}
/**
* Returns the create time as a a \DateTimeInterface
* of this zip entry, or null if no such timestamp exists in the zip entry.
* The milliseconds are always zeroed out, since the underlying data
* offers only per-second precision.
*
* Note: modern linux file systems (e.g., ext2)
* do not appear to store a "create time" value, and so
* it's usually omitted altogether in the zip extra
* field. Perhaps other unix systems track $this->.
*
* @return \DateTimeInterface|null create time as \DateTimeInterface or null
*/
public function getCreateDateTime(): ?\DateTimeInterface
{
return self::timestampToDateTime($this->createTime);
}
/**
* Sets the modify time (seconds since epoch) of this zip entry
* using a integer.
*
* @param int|null $unixTime unix time of the modify time (seconds per epoch) or null
*/
public function setModifyTime(?int $unixTime): void
{
$this->modifyTime = $unixTime;
$this->updateFlags();
}
private function updateFlags(): void
{
$flags = 0;
if ($this->modifyTime !== null) {
$flags |= self::MODIFY_TIME_BIT;
}
if ($this->accessTime !== null) {
$flags |= self::ACCESS_TIME_BIT;
}
if ($this->createTime !== null) {
$flags |= self::CREATE_TIME_BIT;
}
$this->flags = $flags;
}
/**
* Sets the access time (seconds since epoch) of this zip entry
* using a integer.
*
* @param int|null $unixTime Unix time of the access time (seconds per epoch) or null
*/
public function setAccessTime(?int $unixTime): void
{
$this->accessTime = $unixTime;
$this->updateFlags();
}
/**
* Sets the create time (seconds since epoch) of this zip entry
* using a integer.
*
* @param int|null $unixTime Unix time of the create time (seconds per epoch) or null
*/
public function setCreateTime(?int $unixTime): void
{
$this->createTime = $unixTime;
$this->updateFlags();
}
private static function timestampToDateTime(?int $timestamp): ?\DateTimeInterface
{
try {
return $timestamp !== null ? new \DateTimeImmutable('@' . $timestamp) : null;
} catch (\Exception $e) {
return null;
}
}
public function __toString(): string
{
$args = [self::HEADER_ID];
$format = '0x%04x ExtendedTimestamp:';
if ($this->modifyTime !== null) {
$format .= ' Modify:[%s]';
$args[] = date(\DATE_W3C, $this->modifyTime);
}
if ($this->accessTime !== null) {
$format .= ' Access:[%s]';
$args[] = date(\DATE_W3C, $this->accessTime);
}
if ($this->createTime !== null) {
$format .= ' Create:[%s]';
$args[] = date(\DATE_W3C, $this->createTime);
}
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipContainer;
use PhpZip\Model\ZipEntry;
/**
* Jar Marker Extra Field.
* An executable Java program can be packaged in a JAR file with all the libraries it uses.
* Executable JAR files can easily be distinguished from the files packed in the JAR file
* by the extra field in the first file, which is hexadecimal in the 0xCAFE bytes series.
* If this extra field is added as the very first extra field of
* the archive, Solaris will consider it an executable jar file.
*/
final class JarMarkerExtraField implements ZipExtraField
{
/** @var int Header id. */
public const HEADER_ID = 0xCAFE;
public static function setJarMarker(ZipContainer $container): void
{
$zipEntries = $container->getEntries();
if (!empty($zipEntries)) {
foreach ($zipEntries as $zipEntry) {
$zipEntry->removeExtraField(self::HEADER_ID);
}
// set jar execute bit
reset($zipEntries);
$zipEntry = current($zipEntries);
$zipEntry->getCdExtraFields()[] = new self();
}
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
return '';
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
return '';
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException on error
*
* @return JarMarkerExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
if (!empty($buffer)) {
throw new ZipException("JarMarker doesn't expect any data");
}
return new self();
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException on error
*
* @return JarMarkerExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
public function __toString(): string
{
return sprintf('0x%04x Jar Marker', self::HEADER_ID);
}
}

View File

@@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Info-ZIP New Unix Extra Field:
* ====================================.
*
* Currently stores Unix UIDs/GIDs up to 32 bits.
* (Last Revision 20080509)
*
* Value Size Description
* ----- ---- -----------
* (UnixN) 0x7875 Short tag for this extra block type ("ux")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* UIDSize 1 byte Size of UID field
* UID Variable UID for this entry
* GIDSize 1 byte Size of GID field
* GID Variable GID for this entry
*
* Currently Version is set to the number 1. If there is a need
* to change this field, the version will be incremented. Changes
* may not be backward compatible so this extra field should not be
* used if the version is not recognized.
*
* UIDSize is the size of the UID field in bytes. This size should
* match the size of the UID field on the target OS.
*
* UID is the UID for this entry in standard little endian format.
*
* GIDSize is the size of the GID field in bytes. This size should
* match the size of the GID field on the target OS.
*
* GID is the GID for this entry in standard little endian format.
*
* If both the old 16-bit Unix extra field (tag 0x7855, Info-ZIP Unix)
* and this extra field are present, the values in this extra field
* supercede the values in that extra field.
*/
final class NewUnixExtraField implements ZipExtraField
{
/** @var int header id */
public const HEADER_ID = 0x7875;
/** ID of the first non-root user created on a unix system. */
public const USER_GID_PID = 1000;
/** @var int version of this extra field, currently 1 */
private int $version;
/** @var int User id */
private int $uid;
/** @var int Group id */
private int $gid;
public function __construct(int $version = 1, int $uid = self::USER_GID_PID, int $gid = self::USER_GID_PID)
{
$this->version = $version;
$this->uid = $uid;
$this->gid = $gid;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException
*
* @return NewUnixExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
$length = \strlen($buffer);
if ($length < 3) {
throw new ZipException(sprintf('X7875_NewUnix length is too short, only %s bytes', $length));
}
$offset = 0;
[
'version' => $version,
'uidSize' => $uidSize,
] = unpack('Cversion/CuidSize', $buffer);
$offset += 2;
$gid = self::readSizeIntegerLE(substr($buffer, $offset, $uidSize), $uidSize);
$offset += $uidSize;
$gidSize = unpack('C', $buffer[$offset])[1];
$offset++;
$uid = self::readSizeIntegerLE(substr($buffer, $offset, $gidSize), $gidSize);
return new self($version, $gid, $uid);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException
*
* @return NewUnixExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
return pack(
'CCVCV',
$this->version,
4, // UIDSize
$this->uid,
4, // GIDSize
$this->gid
);
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
return $this->packLocalFileData();
}
/**
* @throws ZipException
*/
private static function readSizeIntegerLE(string $data, int $size): int
{
$format = [
1 => 'C', // unsigned byte
2 => 'v', // unsigned short LE
4 => 'V', // unsigned int LE
];
if (!isset($format[$size])) {
throw new ZipException(sprintf('Invalid size bytes: %d', $size));
}
return unpack($format[$size], $data)[1];
}
public function getUid(): int
{
return $this->uid;
}
public function setUid(int $uid): void
{
$this->uid = $uid & 0xFFFFFFFF;
}
public function getGid(): int
{
return $this->gid;
}
public function setGid(int $gid): void
{
$this->gid = $gid & 0xFFFFFFFF;
}
public function getVersion(): int
{
return $this->version;
}
public function __toString(): string
{
return sprintf(
'0x%04x NewUnix: UID=%d GID=%d',
self::HEADER_ID,
$this->uid,
$this->gid
);
}
}

View File

@@ -0,0 +1,287 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* NTFS Extra Field.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
*/
final class NtfsExtraField implements ZipExtraField
{
/** @var int Header id */
public const HEADER_ID = 0x000A;
/** @var int Tag ID */
public const TIME_ATTR_TAG = 0x0001;
/** @var int Attribute size */
public const TIME_ATTR_SIZE = 24; // 3 * 8
/**
* @var int A file time is a 64-bit value that represents the number of
* 100-nanosecond intervals that have elapsed since 12:00
* A.M. January 1, 1601 Coordinated Universal Time (UTC).
* this is the offset of Windows time 0 to Unix epoch in 100-nanosecond intervals.
*/
public const EPOCH_OFFSET = -116444736000000000;
/** @var int Modify ntfs time */
private int $modifyNtfsTime;
/** @var int Access ntfs time */
private int $accessNtfsTime;
/** @var int Create ntfs time */
private int $createNtfsTime;
public function __construct(int $modifyNtfsTime, int $accessNtfsTime, int $createNtfsTime)
{
$this->modifyNtfsTime = $modifyNtfsTime;
$this->accessNtfsTime = $accessNtfsTime;
$this->createNtfsTime = $createNtfsTime;
}
/**
* @return NtfsExtraField
*/
public static function create(
\DateTimeInterface $modifyDateTime,
\DateTimeInterface $accessDateTime,
\DateTimeInterface $createNtfsTime
): self {
return new self(
self::dateTimeToNtfsTime($modifyDateTime),
self::dateTimeToNtfsTime($accessDateTime),
self::dateTimeToNtfsTime($createNtfsTime)
);
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException
*
* @return NtfsExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
if (\PHP_INT_SIZE === 4) {
throw new ZipException('not supported for php-32bit');
}
$buffer = substr($buffer, 4);
$modifyTime = 0;
$accessTime = 0;
$createTime = 0;
while ($buffer || $buffer !== '') {
[
'tag' => $tag,
'sizeAttr' => $sizeAttr,
] = unpack('vtag/vsizeAttr', $buffer);
if ($tag === self::TIME_ATTR_TAG && $sizeAttr === self::TIME_ATTR_SIZE) {
[
'modifyTime' => $modifyTime,
'accessTime' => $accessTime,
'createTime' => $createTime,
] = unpack('PmodifyTime/PaccessTime/PcreateTime', substr($buffer, 4, 24));
break;
}
$buffer = substr($buffer, 4 + $sizeAttr);
}
return new self($modifyTime, $accessTime, $createTime);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @throws ZipException
*
* @return NtfsExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
return pack(
'VvvPPP',
0,
self::TIME_ATTR_TAG,
self::TIME_ATTR_SIZE,
$this->modifyNtfsTime,
$this->accessNtfsTime,
$this->createNtfsTime
);
}
public function getModifyNtfsTime(): int
{
return $this->modifyNtfsTime;
}
public function setModifyNtfsTime(int $modifyNtfsTime): void
{
$this->modifyNtfsTime = $modifyNtfsTime;
}
public function getAccessNtfsTime(): int
{
return $this->accessNtfsTime;
}
public function setAccessNtfsTime(int $accessNtfsTime): void
{
$this->accessNtfsTime = $accessNtfsTime;
}
public function getCreateNtfsTime(): int
{
return $this->createNtfsTime;
}
public function setCreateNtfsTime(int $createNtfsTime): void
{
$this->createNtfsTime = $createNtfsTime;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
return $this->packLocalFileData();
}
public function getModifyDateTime(): \DateTimeInterface
{
return self::ntfsTimeToDateTime($this->modifyNtfsTime);
}
public function setModifyDateTime(\DateTimeInterface $modifyTime): void
{
$this->modifyNtfsTime = self::dateTimeToNtfsTime($modifyTime);
}
public function getAccessDateTime(): \DateTimeInterface
{
return self::ntfsTimeToDateTime($this->accessNtfsTime);
}
public function setAccessDateTime(\DateTimeInterface $accessTime): void
{
$this->accessNtfsTime = self::dateTimeToNtfsTime($accessTime);
}
public function getCreateDateTime(): \DateTimeInterface
{
return self::ntfsTimeToDateTime($this->createNtfsTime);
}
public function setCreateDateTime(\DateTimeInterface $createTime): void
{
$this->createNtfsTime = self::dateTimeToNtfsTime($createTime);
}
/**
* @param float $timestamp Float timestamp
*/
public static function timestampToNtfsTime(float $timestamp): int
{
return (int) (($timestamp * 10000000) - self::EPOCH_OFFSET);
}
public static function dateTimeToNtfsTime(\DateTimeInterface $dateTime): int
{
return self::timestampToNtfsTime((float) $dateTime->format('U.u'));
}
/**
* @return float Float unix timestamp
*/
public static function ntfsTimeToTimestamp(int $ntfsTime): float
{
return (float) (($ntfsTime + self::EPOCH_OFFSET) / 10000000);
}
public static function ntfsTimeToDateTime(int $ntfsTime): \DateTimeInterface
{
$timestamp = self::ntfsTimeToTimestamp($ntfsTime);
$dateTime = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6f', $timestamp));
if ($dateTime === false) {
throw new InvalidArgumentException('Cannot create date/time object for timestamp ' . $timestamp);
}
return $dateTime;
}
public function __toString(): string
{
$args = [self::HEADER_ID];
$format = '0x%04x NtfsExtra:';
if ($this->modifyNtfsTime !== 0) {
$format .= ' Modify:[%s]';
$args[] = $this->getModifyDateTime()->format(\DATE_ATOM);
}
if ($this->accessNtfsTime !== 0) {
$format .= ' Access:[%s]';
$args[] = $this->getAccessDateTime()->format(\DATE_ATOM);
}
if ($this->createNtfsTime !== 0) {
$format .= ' Create:[%s]';
$args[] = $this->getCreateDateTime()->format(\DATE_ATOM);
}
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,295 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Info-ZIP Unix Extra Field (type 1):
* ==================================.
*
* The following is the layout of the old Info-ZIP extra block for
* Unix. It has been replaced by the extended-timestamp extra block
* (0x5455) and the Unix type 2 extra block (0x7855).
* (Last Revision 19970118)
*
* Local-header version:
*
* Value Size Description
* ----- ---- -----------
* (Unix1) 0x5855 Short tag for this extra block type ("UX")
* TSize Short total data size for this block
* AcTime Long time of last access (UTC/GMT)
* ModTime Long time of last modification (UTC/GMT)
* UID Short Unix user ID (optional)
* GID Short Unix group ID (optional)
*
* Central-header version:
*
* Value Size Description
* ----- ---- -----------
* (Unix1) 0x5855 Short tag for this extra block type ("UX")
* TSize Short total data size for this block
* AcTime Long time of last access (GMT/UTC)
* ModTime Long time of last modification (GMT/UTC)
*
* The file access and modification times are in standard Unix signed-
* long format, indicating the number of seconds since 1 January 1970
* 00:00:00. The times are relative to Coordinated Universal Time
* (UTC), also sometimes referred to as Greenwich Mean Time (GMT). To
* convert to local time, the software must know the local timezone
* offset from UTC/GMT. The modification time may be used by non-Unix
* systems to support inter-timezone freshening and updating of zip
* archives.
*
* The local-header extra block may optionally contain UID and GID
* info for the file. The local-header TSize value is the only
* indication of this. Note that Unix UIDs and GIDs are usually
* specific to a particular machine, and they generally require root
* access to restore.
*
* This extra field type is obsolete, but it has been in use since
* mid-1994. Therefore future archiving software should continue to
* support it.
*/
final class OldUnixExtraField implements ZipExtraField
{
/** @var int Header id */
public const HEADER_ID = 0x5855;
/** @var int|null Access timestamp */
private ?int $accessTime;
/** @var int|null Modify timestamp */
private ?int $modifyTime;
/** @var int|null User id */
private ?int $uid;
/** @var int|null Group id */
private ?int $gid;
public function __construct(?int $accessTime, ?int $modifyTime, ?int $uid, ?int $gid)
{
$this->accessTime = $accessTime;
$this->modifyTime = $modifyTime;
$this->uid = $uid;
$this->gid = $gid;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return OldUnixExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
$length = \strlen($buffer);
$accessTime = $modifyTime = $uid = $gid = null;
if ($length >= 4) {
$accessTime = unpack('V', $buffer)[1];
}
if ($length >= 8) {
$modifyTime = unpack('V', substr($buffer, 4, 4))[1];
}
if ($length >= 10) {
$uid = unpack('v', substr($buffer, 8, 2))[1];
}
if ($length >= 12) {
$gid = unpack('v', substr($buffer, 10, 2))[1];
}
return new self($accessTime, $modifyTime, $uid, $gid);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return OldUnixExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
$length = \strlen($buffer);
$accessTime = $modifyTime = null;
if ($length >= 4) {
$accessTime = unpack('V', $buffer)[1];
}
if ($length >= 8) {
$modifyTime = unpack('V', substr($buffer, 4, 4))[1];
}
return new self($accessTime, $modifyTime, null, null);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
$data = '';
if ($this->accessTime !== null) {
$data .= pack('V', $this->accessTime);
if ($this->modifyTime !== null) {
$data .= pack('V', $this->modifyTime);
if ($this->uid !== null) {
$data .= pack('v', $this->uid);
if ($this->gid !== null) {
$data .= pack('v', $this->gid);
}
}
}
}
return $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
$data = '';
if ($this->accessTime !== null) {
$data .= pack('V', $this->accessTime);
if ($this->modifyTime !== null) {
$data .= pack('V', $this->modifyTime);
}
}
return $data;
}
public function getAccessTime(): ?int
{
return $this->accessTime;
}
public function setAccessTime(?int $accessTime): void
{
$this->accessTime = $accessTime;
}
public function getAccessDateTime(): ?\DateTimeInterface
{
try {
return $this->accessTime === null ? null
: new \DateTimeImmutable('@' . $this->accessTime);
} catch (\Exception $e) {
return null;
}
}
public function getModifyTime(): ?int
{
return $this->modifyTime;
}
public function setModifyTime(?int $modifyTime): void
{
$this->modifyTime = $modifyTime;
}
public function getModifyDateTime(): ?\DateTimeInterface
{
try {
return $this->modifyTime === null ? null
: new \DateTimeImmutable('@' . $this->modifyTime);
} catch (\Exception $e) {
return null;
}
}
public function getUid(): ?int
{
return $this->uid;
}
public function setUid(?int $uid): void
{
$this->uid = $uid;
}
public function getGid(): ?int
{
return $this->gid;
}
public function setGid(?int $gid): void
{
$this->gid = $gid;
}
public function __toString(): string
{
$args = [self::HEADER_ID];
$format = '0x%04x OldUnix:';
if (($modifyTime = $this->getModifyDateTime()) !== null) {
$format .= ' Modify:[%s]';
$args[] = $modifyTime->format(\DATE_ATOM);
}
if (($accessTime = $this->getAccessDateTime()) !== null) {
$format .= ' Access:[%s]';
$args[] = $accessTime->format(\DATE_ATOM);
}
if ($this->uid !== null) {
$format .= ' UID=%d';
$args[] = $this->uid;
}
if ($this->gid !== null) {
$format .= ' GID=%d';
$args[] = $this->gid;
}
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
/**
* Info-ZIP Unicode Comment Extra Field (0x6375):.
*
* Stores the UTF-8 version of the file comment as stored in the
* central directory header. (Last Revision 20070912)
*
* Value Size Description
* ----- ---- -----------
* (UCom) 0x6375 Short tag for this extra block type ("uc")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* ComCRC32 4 bytes Comment Field CRC32 Checksum
* UnicodeCom Variable UTF-8 version of the entry comment
*
* Currently Version is set to the number 1. If there is a need
* to change this field, the version will be incremented. Changes
* may not be backward compatible so this extra field should not be
* used if the version is not recognized.
*
* The ComCRC32 is the standard zip CRC32 checksum of the File Comment
* field in the central directory header. This is used to verify that
* the comment field has not changed since the Unicode Comment extra field
* was created. This can happen if a utility changes the File Comment
* field but does not update the UTF-8 Comment extra field. If the CRC
* check fails, this Unicode Comment extra field should be ignored and
* the File Comment field in the header should be used instead.
*
* The UnicodeCom field is the UTF-8 version of the File Comment field
* in the header. As UnicodeCom is defined to be UTF-8, no UTF-8 byte
* order mark (BOM) is used. The length of this field is determined by
* subtracting the size of the previous fields from TSize. If both the
* File Name and Comment fields are UTF-8, the new General Purpose Bit
* Flag, bit 11 (Language encoding flag (EFS)), can be used to indicate
* both the header File Name and Comment fields are UTF-8 and, in this
* case, the Unicode Path and Unicode Comment extra fields are not
* needed and should not be created. Note that, for backward
* compatibility, bit 11 should only be used if the native character set
* of the paths and comments being zipped up are already in UTF-8. It is
* expected that the same file comment storage method, either general
* purpose bit 11 or extra fields, be used in both the Local and Central
* Directory Header for a file.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.6.8
*/
final class UnicodeCommentExtraField extends AbstractUnicodeExtraField
{
public const HEADER_ID = 0x6375;
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
public function __toString(): string
{
return sprintf(
'0x%04x UnicodeComment: "%s"',
self::HEADER_ID,
$this->getUnicodeValue()
);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
/**
* Info-ZIP Unicode Path Extra Field (0x7075):
* ==========================================.
*
* Stores the UTF-8 version of the file name field as stored in the
* local header and central directory header. (Last Revision 20070912)
*
* Value Size Description
* ----- ---- -----------
* (UPath) 0x7075 Short tag for this extra block type ("up")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* NameCRC32 4 bytes File Name Field CRC32 Checksum
* UnicodeName Variable UTF-8 version of the entry File Name
*
* Currently Version is set to the number 1. If there is a need
* to change this field, the version will be incremented. Changes
* may not be backward compatible so this extra field should not be
* used if the version is not recognized.
*
* The NameCRC32 is the standard zip CRC32 checksum of the File Name
* field in the header. This is used to verify that the header
* File Name field has not changed since the Unicode Path extra field
* was created. This can happen if a utility renames the File Name but
* does not update the UTF-8 path extra field. If the CRC check fails,
* this UTF-8 Path Extra Field should be ignored and the File Name field
* in the header should be used instead.
*
* The UnicodeName is the UTF-8 version of the contents of the File Name
* field in the header. As UnicodeName is defined to be UTF-8, no UTF-8
* byte order mark (BOM) is used. The length of this field is determined
* by subtracting the size of the previous fields from TSize. If both
* the File Name and Comment fields are UTF-8, the new General Purpose
* Bit Flag, bit 11 (Language encoding flag (EFS)), can be used to
* indicate that both the header File Name and Comment fields are UTF-8
* and, in this case, the Unicode Path and Unicode Comment extra fields
* are not needed and should not be created. Note that, for backward
* compatibility, bit 11 should only be used if the native character set
* of the paths and comments being zipped up are already in UTF-8. It is
* expected that the same file name storage method, either general
* purpose bit 11 or extra fields, be used in both the Local and Central
* Directory Header for a file.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.6.9
*/
final class UnicodePathExtraField extends AbstractUnicodeExtraField
{
public const HEADER_ID = 0x7075;
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
public function __toString(): string
{
return sprintf(
'0x%04x UnicodePath: "%s"',
self::HEADER_ID,
$this->getUnicodeValue()
);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\RuntimeException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Simple placeholder for all those extra fields we don't want to deal with.
*/
final class UnrecognizedExtraField implements ZipExtraField
{
private int $headerId;
/** @var string extra field data without Header-ID or length specifier */
private string $data;
public function __construct(int $headerId, string $data)
{
$this->headerId = $headerId;
$this->data = $data;
}
public function setHeaderId(int $headerId): void
{
$this->headerId = $headerId;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return $this->headerId;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return UnrecognizedExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
throw new RuntimeException('Unsupport parse');
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return UnrecognizedExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
throw new RuntimeException('Unsupport parse');
}
/**
* {@inheritDoc}
*/
public function packLocalFileData(): string
{
return $this->data;
}
/**
* {@inheritDoc}
*/
public function packCentralDirData(): string
{
return $this->data;
}
public function getData(): string
{
return $this->data;
}
public function setData(string $data): void
{
$this->data = $data;
}
public function __toString(): string
{
$args = [$this->headerId, $this->data];
$format = '0x%04x Unrecognized Extra Field: "%s"';
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,356 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Constants\ZipCompressionMethod;
use PhpZip\Constants\ZipEncryptionMethod;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipUnsupportMethodException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* WinZip AES Extra Field.
*
* @see http://www.winzip.com/win/en/aes_tips.htm AES Coding Tips for Developers
*/
final class WinZipAesExtraField implements ZipExtraField
{
/** @var int Header id */
public const HEADER_ID = 0x9901;
/**
* @var int Data size (currently 7, but subject to possible increase
* in the future)
*/
public const DATA_SIZE = 7;
/**
* @var int The vendor ID field should always be set to the two ASCII
* characters "AE"
*/
public const VENDOR_ID = 0x4541; // 'A' | ('E' << 8)
/**
* @var int Entries of this type do include the standard ZIP CRC-32 value.
* For use with {@see WinZipAesExtraField::setVendorVersion()}.
*/
public const VERSION_AE1 = 1;
/**
* @var int Entries of this type do not include the standard ZIP CRC-32 value.
* For use with {@see WinZipAesExtraField::setVendorVersion().
*/
public const VERSION_AE2 = 2;
/** @var int integer mode value indicating AES encryption 128-bit strength */
public const KEY_STRENGTH_128BIT = 0x01;
/** @var int integer mode value indicating AES encryption 192-bit strength */
public const KEY_STRENGTH_192BIT = 0x02;
/** @var int integer mode value indicating AES encryption 256-bit strength */
public const KEY_STRENGTH_256BIT = 0x03;
/** @var int[] */
private const ALLOW_VENDOR_VERSIONS = [
self::VERSION_AE1,
self::VERSION_AE2,
];
/** @var array<int, int> */
private const ENCRYPTION_STRENGTHS = [
self::KEY_STRENGTH_128BIT => 128,
self::KEY_STRENGTH_192BIT => 192,
self::KEY_STRENGTH_256BIT => 256,
];
/** @var array<int, int> */
private const MAP_KEY_STRENGTH_METHODS = [
self::KEY_STRENGTH_128BIT => ZipEncryptionMethod::WINZIP_AES_128,
self::KEY_STRENGTH_192BIT => ZipEncryptionMethod::WINZIP_AES_192,
self::KEY_STRENGTH_256BIT => ZipEncryptionMethod::WINZIP_AES_256,
];
/** @var int Integer version number specific to the zip vendor */
private int $vendorVersion = self::VERSION_AE1;
/** @var int Integer mode value indicating AES encryption strength */
private int $keyStrength = self::KEY_STRENGTH_256BIT;
/** @var int The actual compression method used to compress the file */
private int $compressionMethod;
/**
* @param int $vendorVersion Integer version number specific to the zip vendor
* @param int $keyStrength Integer mode value indicating AES encryption strength
* @param int $compressionMethod The actual compression method used to compress the file
*
* @throws ZipUnsupportMethodException
*/
public function __construct(int $vendorVersion, int $keyStrength, int $compressionMethod)
{
$this->setVendorVersion($vendorVersion);
$this->setKeyStrength($keyStrength);
$this->setCompressionMethod($compressionMethod);
}
/**
* @throws ZipUnsupportMethodException
*
* @return WinZipAesExtraField
*/
public static function create(ZipEntry $entry): self
{
$keyStrength = array_search($entry->getEncryptionMethod(), self::MAP_KEY_STRENGTH_METHODS, true);
if ($keyStrength === false) {
throw new InvalidArgumentException('Not support encryption method ' . $entry->getEncryptionMethod());
}
// WinZip 11 will continue to use AE-2, with no CRC, for very small files
// of less than 20 bytes. It will also use AE-2 for files compressed in
// BZIP2 format, because this format has internal integrity checks
// equivalent to a CRC check built in.
//
// https://www.winzip.com/win/en/aes_info.html
$vendorVersion = (
$entry->getUncompressedSize() < 20
|| $entry->getCompressionMethod() === ZipCompressionMethod::BZIP2
)
? self::VERSION_AE2
: self::VERSION_AE1;
$field = new self($vendorVersion, $keyStrength, $entry->getCompressionMethod());
$entry->getLocalExtraFields()->add($field);
$entry->getCdExtraFields()->add($field);
return $field;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ?ZipEntry $entry
*
* @throws ZipException on error
*
* @return WinZipAesExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
$size = \strlen($buffer);
if ($size !== self::DATA_SIZE) {
throw new ZipException(
sprintf(
'WinZip AES Extra data invalid size: %d. Must be %d',
$size,
self::DATA_SIZE
)
);
}
[
'vendorVersion' => $vendorVersion,
'vendorId' => $vendorId,
'keyStrength' => $keyStrength,
'compressionMethod' => $compressionMethod,
] = unpack('vvendorVersion/vvendorId/ckeyStrength/vcompressionMethod', $buffer);
if ($vendorId !== self::VENDOR_ID) {
throw new ZipException(
sprintf(
'Vendor id invalid: %d. Must be %d',
$vendorId,
self::VENDOR_ID
)
);
}
return new self($vendorVersion, $keyStrength, $compressionMethod);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ?ZipEntry $entry
*
* @throws ZipException
*
* @return WinZipAesExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
return pack(
'vvcv',
$this->vendorVersion,
self::VENDOR_ID,
$this->keyStrength,
$this->compressionMethod
);
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
return $this->packLocalFileData();
}
/**
* Returns the vendor version.
*
* @see WinZipAesExtraField::VERSION_AE2
* @see WinZipAesExtraField::VERSION_AE1
*/
public function getVendorVersion(): int
{
return $this->vendorVersion;
}
/**
* Sets the vendor version.
*
* @param int $vendorVersion the vendor version
*
* @see WinZipAesExtraField::VERSION_AE2
* @see WinZipAesExtraField::VERSION_AE1
*/
public function setVendorVersion(int $vendorVersion): void
{
if (!\in_array($vendorVersion, self::ALLOW_VENDOR_VERSIONS, true)) {
throw new InvalidArgumentException(
sprintf(
'Unsupport WinZip AES vendor version: %d',
$vendorVersion
)
);
}
$this->vendorVersion = $vendorVersion;
}
/**
* Returns vendor id.
*/
public function getVendorId(): int
{
return self::VENDOR_ID;
}
public function getKeyStrength(): int
{
return $this->keyStrength;
}
/**
* Set key strength.
*/
public function setKeyStrength(int $keyStrength): void
{
if (!isset(self::ENCRYPTION_STRENGTHS[$keyStrength])) {
throw new InvalidArgumentException(
sprintf(
'Key strength %d not support value. Allow values: %s',
$keyStrength,
implode(', ', array_keys(self::ENCRYPTION_STRENGTHS))
)
);
}
$this->keyStrength = $keyStrength;
}
public function getCompressionMethod(): int
{
return $this->compressionMethod;
}
/**
* @throws ZipUnsupportMethodException
*/
public function setCompressionMethod(int $compressionMethod): void
{
ZipCompressionMethod::checkSupport($compressionMethod);
$this->compressionMethod = $compressionMethod;
}
public function getEncryptionStrength(): int
{
return self::ENCRYPTION_STRENGTHS[$this->keyStrength];
}
public function getEncryptionMethod(): int
{
$keyStrength = $this->getKeyStrength();
if (!isset(self::MAP_KEY_STRENGTH_METHODS[$keyStrength])) {
throw new InvalidArgumentException('Invalid encryption method');
}
return self::MAP_KEY_STRENGTH_METHODS[$keyStrength];
}
public function isV1(): bool
{
return $this->vendorVersion === self::VERSION_AE1;
}
public function isV2(): bool
{
return $this->vendorVersion === self::VERSION_AE2;
}
public function getSaltSize(): int
{
return (int) ($this->getEncryptionStrength() / 8 / 2);
}
public function __toString(): string
{
return sprintf(
'0x%04x WINZIP AES: VendorVersion=%d KeyStrength=0x%02x CompressionMethod=%s',
__CLASS__,
$this->vendorVersion,
$this->keyStrength,
$this->compressionMethod
);
}
}

View File

@@ -0,0 +1,277 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Constants\ZipConstants;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* ZIP64 Extra Field.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
*/
final class Zip64ExtraField implements ZipExtraField
{
/** @var int The Header ID for a ZIP64 Extended Information Extra Field. */
public const HEADER_ID = 0x0001;
private ?int $uncompressedSize;
private ?int $compressedSize;
private ?int $localHeaderOffset;
private ?int $diskStart;
public function __construct(
?int $uncompressedSize = null,
?int $compressedSize = null,
?int $localHeaderOffset = null,
?int $diskStart = null
) {
$this->uncompressedSize = $uncompressedSize;
$this->compressedSize = $compressedSize;
$this->localHeaderOffset = $localHeaderOffset;
$this->diskStart = $diskStart;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ?ZipEntry $entry
*
* @throws ZipException on error
*
* @return Zip64ExtraField
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self
{
$length = \strlen($buffer);
if ($length === 0) {
// no local file data at all, may happen if an archive
// only holds a ZIP64 extended information extra field
// inside the central directory but not inside the local
// file header
return new self();
}
if ($length < 16) {
throw new ZipException(
'Zip64 extended information must contain both size values in the local file header.'
);
}
[
'uncompressedSize' => $uncompressedSize,
'compressedSize' => $compressedSize,
] = unpack('PuncompressedSize/PcompressedSize', substr($buffer, 0, 16));
return new self($uncompressedSize, $compressedSize);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ?ZipEntry $entry
*
* @throws ZipException
*
* @return Zip64ExtraField
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self
{
if ($entry === null) {
throw new RuntimeException('zipEntry is null');
}
$length = \strlen($buffer);
$remaining = $length;
$uncompressedSize = null;
$compressedSize = null;
$localHeaderOffset = null;
$diskStart = null;
if ($entry->getUncompressedSize() === ZipConstants::ZIP64_MAGIC) {
if ($remaining < 8) {
throw new ZipException('ZIP64 extension corrupt (no uncompressed size).');
}
$uncompressedSize = unpack('P', substr($buffer, $length - $remaining, 8))[1];
$remaining -= 8;
}
if ($entry->getCompressedSize() === ZipConstants::ZIP64_MAGIC) {
if ($remaining < 8) {
throw new ZipException('ZIP64 extension corrupt (no compressed size).');
}
$compressedSize = unpack('P', substr($buffer, $length - $remaining, 8))[1];
$remaining -= 8;
}
if ($entry->getLocalHeaderOffset() === ZipConstants::ZIP64_MAGIC) {
if ($remaining < 8) {
throw new ZipException('ZIP64 extension corrupt (no relative local header offset).');
}
$localHeaderOffset = unpack('P', substr($buffer, $length - $remaining, 8))[1];
$remaining -= 8;
}
if ($remaining === 4) {
$diskStart = unpack('V', substr($buffer, $length - $remaining, 4))[1];
}
return new self($uncompressedSize, $compressedSize, $localHeaderOffset, $diskStart);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string
{
if ($this->uncompressedSize !== null || $this->compressedSize !== null) {
if ($this->uncompressedSize === null || $this->compressedSize === null) {
throw new \InvalidArgumentException(
'Zip64 extended information must contain both size values in the local file header.'
);
}
return $this->packSizes();
}
return '';
}
private function packSizes(): string
{
$data = '';
if ($this->uncompressedSize !== null) {
$data .= pack('P', $this->uncompressedSize);
}
if ($this->compressedSize !== null) {
$data .= pack('P', $this->compressedSize);
}
return $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string
{
$data = $this->packSizes();
if ($this->localHeaderOffset !== null) {
$data .= pack('P', $this->localHeaderOffset);
}
if ($this->diskStart !== null) {
$data .= pack('V', $this->diskStart);
}
return $data;
}
public function getUncompressedSize(): ?int
{
return $this->uncompressedSize;
}
public function setUncompressedSize(?int $uncompressedSize): void
{
$this->uncompressedSize = $uncompressedSize;
}
public function getCompressedSize(): ?int
{
return $this->compressedSize;
}
public function setCompressedSize(?int $compressedSize): void
{
$this->compressedSize = $compressedSize;
}
public function getLocalHeaderOffset(): ?int
{
return $this->localHeaderOffset;
}
public function setLocalHeaderOffset(?int $localHeaderOffset): void
{
$this->localHeaderOffset = $localHeaderOffset;
}
public function getDiskStart(): ?int
{
return $this->diskStart;
}
public function setDiskStart(?int $diskStart): void
{
$this->diskStart = $diskStart;
}
public function __toString(): string
{
$args = [self::HEADER_ID];
$format = '0x%04x ZIP64: ';
$formats = [];
if ($this->uncompressedSize !== null) {
$formats[] = 'SIZE=%d';
$args[] = $this->uncompressedSize;
}
if ($this->compressedSize !== null) {
$formats[] = 'COMP_SIZE=%d';
$args[] = $this->compressedSize;
}
if ($this->localHeaderOffset !== null) {
$formats[] = 'OFFSET=%d';
$args[] = $this->localHeaderOffset;
}
if ($this->diskStart !== null) {
$formats[] = 'DISK_START=%d';
$args[] = $this->diskStart;
}
$format .= implode(' ', $formats);
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField;
use PhpZip\Model\Extra\Fields\AsiExtraField;
use PhpZip\Model\Extra\Fields\ExtendedTimestampExtraField;
use PhpZip\Model\Extra\Fields\JarMarkerExtraField;
use PhpZip\Model\Extra\Fields\NewUnixExtraField;
use PhpZip\Model\Extra\Fields\NtfsExtraField;
use PhpZip\Model\Extra\Fields\OldUnixExtraField;
use PhpZip\Model\Extra\Fields\UnicodeCommentExtraField;
use PhpZip\Model\Extra\Fields\UnicodePathExtraField;
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
use PhpZip\Model\Extra\Fields\Zip64ExtraField;
/**
* Class ZipExtraManager.
*/
final class ZipExtraDriver
{
/**
* @var array<int, string>
* @psalm-var array<int, class-string<ZipExtraField>>
*/
private static array $implementations = [
ApkAlignmentExtraField::HEADER_ID => ApkAlignmentExtraField::class,
AsiExtraField::HEADER_ID => AsiExtraField::class,
ExtendedTimestampExtraField::HEADER_ID => ExtendedTimestampExtraField::class,
JarMarkerExtraField::HEADER_ID => JarMarkerExtraField::class,
NewUnixExtraField::HEADER_ID => NewUnixExtraField::class,
NtfsExtraField::HEADER_ID => NtfsExtraField::class,
OldUnixExtraField::HEADER_ID => OldUnixExtraField::class,
UnicodeCommentExtraField::HEADER_ID => UnicodeCommentExtraField::class,
UnicodePathExtraField::HEADER_ID => UnicodePathExtraField::class,
WinZipAesExtraField::HEADER_ID => WinZipAesExtraField::class,
Zip64ExtraField::HEADER_ID => Zip64ExtraField::class,
];
private function __construct()
{
}
/**
* @param string|ZipExtraField $extraField ZipExtraField object or class name
*/
public static function register($extraField): void
{
if (!is_a($extraField, ZipExtraField::class, true)) {
throw new InvalidArgumentException(
sprintf(
'$extraField "%s" is not implements interface %s',
(string) $extraField,
ZipExtraField::class
)
);
}
self::$implementations[\call_user_func([$extraField, 'getHeaderId'])] = $extraField;
}
/**
* @param int|string|ZipExtraField $extraType ZipExtraField object or class name or extra header id
*/
public static function unregister($extraType): bool
{
$headerId = null;
if (\is_int($extraType)) {
$headerId = $extraType;
} elseif (is_a($extraType, ZipExtraField::class, true)) {
$headerId = \call_user_func([$extraType, 'getHeaderId']);
} else {
return false;
}
if (isset(self::$implementations[$headerId])) {
unset(self::$implementations[$headerId]);
return true;
}
return false;
}
public static function getClassNameOrNull(int $headerId): ?string
{
if ($headerId < 0 || $headerId > 0xFFFF) {
throw new \InvalidArgumentException('$headerId out of range: ' . $headerId);
}
return self::$implementations[$headerId] ?? null;
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\Model\Extra;
use PhpZip\Model\ZipEntry;
/**
* Extra Field in a Local or Central Header of a ZIP archive.
* It defines the common properties of all Extra Fields and how to
* serialize/unserialize them to/from byte arrays.
*/
interface ZipExtraField
{
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*/
public function getHeaderId(): int;
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return static
*/
public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self;
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry optional zip entry
*
* @return static
*/
public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self;
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData(): string;
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData(): string;
public function __toString(): string;
}