├── .php-cs-fixer.php ├── .phpstorm.meta.php ├── LICENSE ├── README.RU.md ├── README.md ├── composer.json └── src ├── Constants ├── DosAttrs.php ├── DosCodePage.php ├── GeneralPurposeBitFlag.php ├── UnixStat.php ├── ZipCompressionLevel.php ├── ZipCompressionMethod.php ├── ZipConstants.php ├── ZipEncryptionMethod.php ├── ZipOptions.php ├── ZipPlatform.php └── ZipVersion.php ├── Exception ├── Crc32Exception.php ├── InvalidArgumentException.php ├── RuntimeException.php ├── ZipAuthenticationException.php ├── ZipCryptoException.php ├── ZipEntryNotFoundException.php ├── ZipException.php └── ZipUnsupportMethodException.php ├── IO ├── Filter │ └── Cipher │ │ ├── Pkware │ │ ├── PKCryptContext.php │ │ ├── PKDecryptionStreamFilter.php │ │ └── PKEncryptionStreamFilter.php │ │ └── WinZipAes │ │ ├── WinZipAesContext.php │ │ ├── WinZipAesDecryptionStreamFilter.php │ │ └── WinZipAesEncryptionStreamFilter.php ├── Stream │ ├── ResponseStream.php │ └── ZipEntryStreamWrapper.php ├── ZipReader.php └── ZipWriter.php ├── Model ├── Data │ ├── ZipFileData.php │ ├── ZipNewData.php │ └── ZipSourceFileData.php ├── EndOfCentralDirectory.php ├── Extra │ ├── ExtraFieldsCollection.php │ ├── Fields │ │ ├── AbstractUnicodeExtraField.php │ │ ├── ApkAlignmentExtraField.php │ │ ├── AsiExtraField.php │ │ ├── ExtendedTimestampExtraField.php │ │ ├── JarMarkerExtraField.php │ │ ├── NewUnixExtraField.php │ │ ├── NtfsExtraField.php │ │ ├── OldUnixExtraField.php │ │ ├── UnicodeCommentExtraField.php │ │ ├── UnicodePathExtraField.php │ │ ├── UnrecognizedExtraField.php │ │ ├── WinZipAesExtraField.php │ │ └── Zip64ExtraField.php │ ├── ZipExtraDriver.php │ └── ZipExtraField.php ├── ImmutableZipContainer.php ├── ZipContainer.php ├── ZipData.php ├── ZipEntry.php └── ZipEntryMatcher.php ├── Util ├── CryptoUtil.php ├── DateTimeConverter.php ├── FileAttribUtil.php ├── FilesUtil.php ├── Iterator │ ├── IgnoreFilesFilterIterator.php │ └── IgnoreFilesRecursiveFilterIterator.php ├── MathUtil.php └── StringUtil.php └── ZipFile.php /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | interface DosAttrs 15 | { 16 | /** @var int DOS File Attribute Read Only */ 17 | public const DOS_READ_ONLY = 0x01; 18 | 19 | /** @var int DOS File Attribute Hidden */ 20 | public const DOS_HIDDEN = 0x02; 21 | 22 | /** @var int DOS File Attribute System */ 23 | public const DOS_SYSTEM = 0x04; 24 | 25 | /** @var int DOS File Attribute Label */ 26 | public const DOS_LABEL = 0x08; 27 | 28 | /** @var int DOS File Attribute Directory */ 29 | public const DOS_DIRECTORY = 0x10; 30 | 31 | /** @var int DOS File Attribute Archive */ 32 | public const DOS_ARCHIVE = 0x20; 33 | 34 | /** @var int DOS File Attribute Link */ 35 | public const DOS_LINK = 0x40; 36 | 37 | /** @var int DOS File Attribute Execute */ 38 | public const DOS_EXE = 0x80; 39 | } 40 | -------------------------------------------------------------------------------- /src/Constants/DosCodePage.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | final class DosCodePage 15 | { 16 | public const CP_LATIN_US = 'cp437'; 17 | 18 | public const CP_GREEK = 'cp737'; 19 | 20 | public const CP_BALT_RIM = 'cp775'; 21 | 22 | public const CP_LATIN1 = 'cp850'; 23 | 24 | public const CP_LATIN2 = 'cp852'; 25 | 26 | public const CP_CYRILLIC = 'cp855'; 27 | 28 | public const CP_TURKISH = 'cp857'; 29 | 30 | public const CP_PORTUGUESE = 'cp860'; 31 | 32 | public const CP_ICELANDIC = 'cp861'; 33 | 34 | public const CP_HEBREW = 'cp862'; 35 | 36 | public const CP_CANADA = 'cp863'; 37 | 38 | public const CP_ARABIC = 'cp864'; 39 | 40 | public const CP_NORDIC = 'cp865'; 41 | 42 | public const CP_CYRILLIC_RUSSIAN = 'cp866'; 43 | 44 | public const CP_GREEK2 = 'cp869'; 45 | 46 | public const CP_THAI = 'cp874'; 47 | 48 | /** @var string[] */ 49 | private const CP_CHARSETS = [ 50 | self::CP_LATIN_US, 51 | self::CP_GREEK, 52 | self::CP_BALT_RIM, 53 | self::CP_LATIN1, 54 | self::CP_LATIN2, 55 | self::CP_CYRILLIC, 56 | self::CP_TURKISH, 57 | self::CP_PORTUGUESE, 58 | self::CP_ICELANDIC, 59 | self::CP_HEBREW, 60 | self::CP_CANADA, 61 | self::CP_ARABIC, 62 | self::CP_NORDIC, 63 | self::CP_CYRILLIC_RUSSIAN, 64 | self::CP_GREEK2, 65 | self::CP_THAI, 66 | ]; 67 | 68 | /** 69 | * @noinspection PhpComposerExtensionStubsInspection 70 | */ 71 | public static function toUTF8(string $str, string $sourceEncoding): string 72 | { 73 | $s = iconv($sourceEncoding, 'UTF-8', $str); 74 | 75 | if ($s === false) { 76 | return $str; 77 | } 78 | 79 | return $s; 80 | } 81 | 82 | /** 83 | * @noinspection PhpComposerExtensionStubsInspection 84 | */ 85 | public static function fromUTF8(string $str, string $destEncoding): string 86 | { 87 | $s = iconv('UTF-8', $destEncoding, $str); 88 | 89 | if ($s === false) { 90 | return $str; 91 | } 92 | 93 | return $s; 94 | } 95 | 96 | /** 97 | * @return string[] 98 | */ 99 | public static function getCodePages(): array 100 | { 101 | return self::CP_CHARSETS; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Constants/GeneralPurposeBitFlag.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | interface GeneralPurposeBitFlag 15 | { 16 | /** 17 | * General Purpose Bit Flag mask for encrypted data. 18 | * Bit 0: If set, indicates that the file is encrypted. 19 | */ 20 | public const ENCRYPTION = 1 << 0; 21 | 22 | /** 23 | * Compression Flag Bit 1 for method Deflating. 24 | * 25 | * Bit 2 Bit 1 26 | * 0 0 Normal compression 27 | * 0 1 Maximum compression 28 | * 1 0 Fast compression 29 | * 1 1 Super Fast compression 30 | * 31 | * @see GeneralPurposeBitFlag::COMPRESSION_FLAG2 32 | */ 33 | public const COMPRESSION_FLAG1 = 1 << 1; 34 | 35 | /** 36 | * Compression Flag Bit 2 for method Deflating. 37 | * 38 | * Bit 2 Bit 1 39 | * 0 0 Normal compression 40 | * 0 1 Maximum compression 41 | * 1 0 Fast compression 42 | * 1 1 Super Fast compression 43 | * 44 | * @see GeneralPurposeBitFlag::COMPRESSION_FLAG1 45 | */ 46 | public const COMPRESSION_FLAG2 = 1 << 2; 47 | 48 | /** 49 | * General Purpose Bit Flag mask for data descriptor. 50 | * 51 | * Bit 3: If this bit is set, the fields crc-32, compressed 52 | * size and uncompressed size are set to zero in the 53 | * local header. The correct values are put in the data 54 | * descriptor immediately following the compressed data. 55 | */ 56 | public const DATA_DESCRIPTOR = 1 << 3; 57 | 58 | /** 59 | * General Purpose Bit Flag mask for strong encryption. 60 | * 61 | * Bit 6: Strong encryption. 62 | * If this bit is set, you MUST set the version needed to extract 63 | * value to at least 50 and you MUST also set bit 0. 64 | * If AES encryption is used, the version needed to extract value 65 | * MUST be at least 51. 66 | */ 67 | public const STRONG_ENCRYPTION = 1 << 6; 68 | 69 | /** 70 | * General Purpose Bit Flag mask for UTF-8. 71 | * 72 | * Bit 11: Language encoding flag (EFS). 73 | * If this bit is set, the filename and comment fields 74 | * for this file MUST be encoded using UTF-8. (see APPENDIX D) 75 | */ 76 | public const UTF8 = 1 << 11; 77 | } 78 | -------------------------------------------------------------------------------- /src/Constants/UnixStat.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | /** 15 | * Unix stat constants. 16 | */ 17 | interface UnixStat 18 | { 19 | /** @var int unix file type mask */ 20 | public const UNX_IFMT = 0170000; 21 | 22 | /** @var int unix regular file */ 23 | public const UNX_IFREG = 0100000; 24 | 25 | /** @var int unix socket (BSD, not SysV or Amiga) */ 26 | public const UNX_IFSOCK = 0140000; 27 | 28 | /** @var int unix symbolic link (not SysV, Amiga) */ 29 | public const UNX_IFLNK = 0120000; 30 | 31 | /** @var int unix block special (not Amiga) */ 32 | public const UNX_IFBLK = 0060000; 33 | 34 | /** @var int unix directory */ 35 | public const UNX_IFDIR = 0040000; 36 | 37 | /** @var int unix character special (not Amiga) */ 38 | public const UNX_IFCHR = 0020000; 39 | 40 | /** @var int unix fifo (BCC, not MSC or Amiga) */ 41 | public const UNX_IFIFO = 0010000; 42 | 43 | /** @var int unix set user id on execution */ 44 | public const UNX_ISUID = 04000; 45 | 46 | /** @var int unix set group id on execution */ 47 | public const UNX_ISGID = 02000; 48 | 49 | /** @var int unix directory permissions control */ 50 | public const UNX_ISVTX = 01000; 51 | 52 | /** @var int unix record locking enforcement flag */ 53 | public const UNX_ENFMT = 02000; 54 | 55 | /** @var int unix read, write, execute: owner */ 56 | public const UNX_IRWXU = 00700; 57 | 58 | /** @var int unix read permission: owner */ 59 | public const UNX_IRUSR = 00400; 60 | 61 | /** @var int unix write permission: owner */ 62 | public const UNX_IWUSR = 00200; 63 | 64 | /** @var int unix execute permission: owner */ 65 | public const UNX_IXUSR = 00100; 66 | 67 | /** @var int unix read, write, execute: group */ 68 | public const UNX_IRWXG = 00070; 69 | 70 | /** @var int unix read permission: group */ 71 | public const UNX_IRGRP = 00040; 72 | 73 | /** @var int unix write permission: group */ 74 | public const UNX_IWGRP = 00020; 75 | 76 | /** @var int unix execute permission: group */ 77 | public const UNX_IXGRP = 00010; 78 | 79 | /** @var int unix read, write, execute: other */ 80 | public const UNX_IRWXO = 00007; 81 | 82 | /** @var int unix read permission: other */ 83 | public const UNX_IROTH = 00004; 84 | 85 | /** @var int unix write permission: other */ 86 | public const UNX_IWOTH = 00002; 87 | 88 | /** @var int unix execute permission: other */ 89 | public const UNX_IXOTH = 00001; 90 | } 91 | -------------------------------------------------------------------------------- /src/Constants/ZipCompressionLevel.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | /** 15 | * Compression levels for Deflate and BZIP2. 16 | * 17 | * {@see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT} Section 4.4.4: 18 | * 19 | * For Methods 8 and 9 - Deflating 20 | * ------------------------------- 21 | * Bit 2 Bit 1 22 | * 0 0 Normal (-en) compression option was used. 23 | * 0 1 Maximum (-exx/-ex) compression option was used. 24 | * 1 0 Fast (-ef) compression option was used. 25 | * 1 1 Super Fast (-es) compression option was used. 26 | * 27 | * Different programs encode compression level information in different ways: 28 | * 29 | * Deflate Compress Level pkzip zip 7z, WinRAR WinZip 30 | * ---------------------- ---------------- ------- ---------- ------ 31 | * Super Fast compression 1 1 32 | * Fast compression 2 1, 2 33 | * Normal Compression 3 - 8 (5 default) 3 - 7 1 - 9 34 | * Maximum compression 9 8, 9 9 35 | */ 36 | interface ZipCompressionLevel 37 | { 38 | /** @var int Compression level for super fast compression. */ 39 | public const SUPER_FAST = 1; 40 | 41 | /** @var int compression level for fast compression */ 42 | public const FAST = 2; 43 | 44 | /** @var int compression level for normal compression */ 45 | public const NORMAL = 5; 46 | 47 | /** @var int compression level for maximum compression */ 48 | public const MAXIMUM = 9; 49 | 50 | /** 51 | * @var int int Minimum compression level 52 | * 53 | * @internal 54 | */ 55 | public const LEVEL_MIN = self::SUPER_FAST; 56 | 57 | /** 58 | * @var int int Maximum compression level 59 | * 60 | * @internal 61 | */ 62 | public const LEVEL_MAX = self::MAXIMUM; 63 | } 64 | -------------------------------------------------------------------------------- /src/Constants/ZipCompressionMethod.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | use PhpZip\Exception\ZipUnsupportMethodException; 15 | 16 | final class ZipCompressionMethod 17 | { 18 | /** @var int Compression method Store */ 19 | public const STORED = 0; 20 | 21 | /** @var int Compression method Deflate */ 22 | public const DEFLATED = 8; 23 | 24 | /** @var int Compression method Bzip2 */ 25 | public const BZIP2 = 12; 26 | 27 | /** @var int Compression method AES-Encryption */ 28 | public const WINZIP_AES = 99; 29 | 30 | /** @var array Compression Methods */ 31 | private const ZIP_COMPRESSION_METHODS = [ 32 | self::STORED => 'Stored', 33 | 1 => 'Shrunk', 34 | 2 => 'Reduced compression factor 1', 35 | 3 => 'Reduced compression factor 2', 36 | 4 => 'Reduced compression factor 3', 37 | 5 => 'Reduced compression factor 4', 38 | 6 => 'Imploded', 39 | 7 => 'Reserved for Tokenizing compression algorithm', 40 | self::DEFLATED => 'Deflated', 41 | 9 => 'Enhanced Deflating using Deflate64(tm)', 42 | 10 => 'PKWARE Data Compression Library Imploding', 43 | 11 => 'Reserved by PKWARE', 44 | self::BZIP2 => 'BZIP2', 45 | 13 => 'Reserved by PKWARE', 46 | 14 => 'LZMA', 47 | 15 => 'Reserved by PKWARE', 48 | 16 => 'Reserved by PKWARE', 49 | 17 => 'Reserved by PKWARE', 50 | 18 => 'File is compressed using IBM TERSE (new)', 51 | 19 => 'IBM LZ77 z Architecture (PFS)', 52 | 96 => 'WinZip JPEG Compression', 53 | 97 => 'WavPack compressed data', 54 | 98 => 'PPMd version I, Rev 1', 55 | self::WINZIP_AES => 'AES Encryption', 56 | ]; 57 | 58 | public static function getCompressionMethodName(int $value): string 59 | { 60 | return self::ZIP_COMPRESSION_METHODS[$value] ?? 'Unknown Method'; 61 | } 62 | 63 | /** 64 | * @return int[] 65 | */ 66 | public static function getSupportMethods(): array 67 | { 68 | static $methods; 69 | 70 | if ($methods === null) { 71 | $methods = [ 72 | self::STORED, 73 | self::DEFLATED, 74 | ]; 75 | 76 | if (\extension_loaded('bz2')) { 77 | $methods[] = self::BZIP2; 78 | } 79 | } 80 | 81 | return $methods; 82 | } 83 | 84 | /** 85 | * @throws ZipUnsupportMethodException 86 | */ 87 | public static function checkSupport(int $compressionMethod): void 88 | { 89 | if (!\in_array($compressionMethod, self::getSupportMethods(), true)) { 90 | throw new ZipUnsupportMethodException(sprintf( 91 | 'Compression method %d (%s) is not supported.', 92 | $compressionMethod, 93 | self::getCompressionMethodName($compressionMethod) 94 | )); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Constants/ZipConstants.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | /** 15 | * Zip Constants. 16 | */ 17 | interface ZipConstants 18 | { 19 | /** @var int End Of Central Directory Record signature. */ 20 | public const END_CD = 0x06054B50; // "PK\005\006" 21 | 22 | /** @var int Zip64 End Of Central Directory Record. */ 23 | public const ZIP64_END_CD = 0x06064B50; // "PK\006\006" 24 | 25 | /** @var int Zip64 End Of Central Directory Locator. */ 26 | public const ZIP64_END_CD_LOC = 0x07064B50; // "PK\006\007" 27 | 28 | /** @var int Central File Header signature. */ 29 | public const CENTRAL_FILE_HEADER = 0x02014B50; // "PK\001\002" 30 | 31 | /** @var int Local File Header signature. */ 32 | public const LOCAL_FILE_HEADER = 0x04034B50; // "PK\003\004" 33 | 34 | /** @var int Data Descriptor signature. */ 35 | public const DATA_DESCRIPTOR = 0x08074B50; // "PK\007\008" 36 | 37 | /** 38 | * @var int value stored in four-byte size and similar fields 39 | * if ZIP64 extensions are used 40 | */ 41 | public const ZIP64_MAGIC = 0xFFFFFFFF; 42 | 43 | /** 44 | * Local File Header signature 4 45 | * Version Needed To Extract 2 46 | * General Purpose Bit Flags 2 47 | * Compression Method 2 48 | * Last Mod File Time 2 49 | * Last Mod File Date 2 50 | * CRC-32 4 51 | * Compressed Size 4 52 | * Uncompressed Size 4. 53 | * 54 | * @var int Local File Header filename position 55 | */ 56 | public const LFH_FILENAME_LENGTH_POS = 26; 57 | 58 | /** 59 | * The minimum length of the Local File Header record. 60 | * 61 | * local file header signature 4 62 | * version needed to extract 2 63 | * general purpose bit flag 2 64 | * compression method 2 65 | * last mod file time 2 66 | * last mod file date 2 67 | * crc-32 4 68 | * compressed size 4 69 | * uncompressed size 4 70 | * file name length 2 71 | * extra field length 2 72 | */ 73 | public const LFH_FILENAME_POS = 30; 74 | 75 | /** @var int the length of the Zip64 End Of Central Directory Locator */ 76 | public const ZIP64_END_CD_LOC_LEN = 20; 77 | 78 | /** @var int the minimum length of the End Of Central Directory Record */ 79 | public const END_CD_MIN_LEN = 22; 80 | 81 | /** 82 | * The minimum length of the Zip64 End Of Central Directory Record. 83 | * 84 | * zip64 end of central dir 85 | * signature 4 86 | * size of zip64 end of central 87 | * directory record 8 88 | * version made by 2 89 | * version needed to extract 2 90 | * number of this disk 4 91 | * number of the disk with the 92 | * start of the central directory 4 93 | * total number of entries in the 94 | * central directory on this disk 8 95 | * total number of entries in 96 | * the central directory 8 97 | * size of the central directory 8 98 | * offset of start of central 99 | * directory with respect to 100 | * the starting disk number 8 101 | * 102 | * @var int ZIP64 End Of Central Directory length 103 | */ 104 | public const ZIP64_END_OF_CD_LEN = 56; 105 | } 106 | -------------------------------------------------------------------------------- /src/Constants/ZipEncryptionMethod.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | use PhpZip\Exception\InvalidArgumentException; 15 | 16 | final class ZipEncryptionMethod 17 | { 18 | public const NONE = -1; 19 | 20 | /** @var int Traditional PKWARE encryption. */ 21 | public const PKWARE = 0; 22 | 23 | /** @var int WinZip AES-256 */ 24 | public const WINZIP_AES_256 = 1; 25 | 26 | /** @var int WinZip AES-128 */ 27 | public const WINZIP_AES_128 = 2; 28 | 29 | /** @var int WinZip AES-192 */ 30 | public const WINZIP_AES_192 = 3; 31 | 32 | /** @var array */ 33 | private const ENCRYPTION_METHODS = [ 34 | self::NONE => 'no encryption', 35 | self::PKWARE => 'Traditional PKWARE encryption', 36 | self::WINZIP_AES_128 => 'WinZip AES-128', 37 | self::WINZIP_AES_192 => 'WinZip AES-192', 38 | self::WINZIP_AES_256 => 'WinZip AES-256', 39 | ]; 40 | 41 | public static function getEncryptionMethodName(int $value): string 42 | { 43 | return self::ENCRYPTION_METHODS[$value] ?? 'Unknown Encryption Method'; 44 | } 45 | 46 | public static function hasEncryptionMethod(int $encryptionMethod): bool 47 | { 48 | return isset(self::ENCRYPTION_METHODS[$encryptionMethod]); 49 | } 50 | 51 | public static function isWinZipAesMethod(int $encryptionMethod): bool 52 | { 53 | return \in_array( 54 | $encryptionMethod, 55 | [ 56 | self::WINZIP_AES_256, 57 | self::WINZIP_AES_192, 58 | self::WINZIP_AES_128, 59 | ], 60 | true 61 | ); 62 | } 63 | 64 | /** 65 | * @throws InvalidArgumentException 66 | */ 67 | public static function checkSupport(int $encryptionMethod): void 68 | { 69 | if (!self::hasEncryptionMethod($encryptionMethod)) { 70 | throw new InvalidArgumentException(sprintf( 71 | 'Encryption method %d is not supported.', 72 | $encryptionMethod 73 | )); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Constants/ZipOptions.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | use PhpZip\IO\ZipReader; 15 | use PhpZip\ZipFile; 16 | 17 | interface ZipOptions 18 | { 19 | /** 20 | * Boolean option for store just file names (skip directory names). 21 | * 22 | * @see ZipFile::addFromFinder() 23 | */ 24 | public const STORE_ONLY_FILES = 'only_files'; 25 | 26 | /** 27 | * Uses the specified compression method. 28 | * 29 | * @see ZipFile::addFromFinder() 30 | * @see ZipFile::addSplFile() 31 | */ 32 | public const COMPRESSION_METHOD = 'compression_method'; 33 | 34 | /** 35 | * Set the specified record modification time. 36 | * The value can be {@see \DateTimeInterface}, integer timestamp 37 | * or a string of any format. 38 | * 39 | * @see ZipFile::addFromFinder() 40 | * @see ZipFile::addSplFile() 41 | */ 42 | public const MODIFIED_TIME = 'mtime'; 43 | 44 | /** 45 | * Specifies the encoding of the record name for cases when the UTF-8 46 | * usage flag is not set. 47 | * 48 | * The most commonly used encodings are compiled into the constants 49 | * of the {@see DosCodePage} class. 50 | * 51 | * @see ZipFile::openFile() 52 | * @see ZipFile::openFromString() 53 | * @see ZipFile::openFromStream() 54 | * @see ZipReader::getDefaultOptions() 55 | * @see DosCodePage::getCodePages() 56 | */ 57 | public const CHARSET = 'charset'; 58 | 59 | /** 60 | * Allows ({@see true}) or denies ({@see false}) unpacking unix symlinks. 61 | * 62 | * This is a potentially dangerous operation for uncontrolled zip files. 63 | * By default is ({@see false}). 64 | * 65 | * @see https://josipfranjkovic.blogspot.com/2014/12/reading-local-files-from-facebooks.html 66 | */ 67 | public const EXTRACT_SYMLINKS = 'extract_symlinks'; 68 | } 69 | -------------------------------------------------------------------------------- /src/Constants/ZipPlatform.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | final class ZipPlatform 15 | { 16 | /** @var int MS-DOS OS */ 17 | public const OS_DOS = 0; 18 | 19 | /** @var int Unix OS */ 20 | public const OS_UNIX = 3; 21 | 22 | /** @var int MacOS platform */ 23 | public const OS_MAC_OSX = 19; 24 | 25 | /** @var array Zip Platforms */ 26 | private const PLATFORMS = [ 27 | self::OS_DOS => 'MS-DOS', 28 | 1 => 'Amiga', 29 | 2 => 'OpenVMS', 30 | self::OS_UNIX => 'Unix', 31 | 4 => 'VM/CMS', 32 | 5 => 'Atari ST', 33 | 6 => 'HPFS (OS/2, NT 3.x)', 34 | 7 => 'Macintosh', 35 | 8 => 'Z-System', 36 | 9 => 'CP/M', 37 | 10 => 'Windows NTFS or TOPS-20', 38 | 11 => 'MVS or NTFS', 39 | 12 => 'VSE or SMS/QDOS', 40 | 13 => 'Acorn RISC OS', 41 | 14 => 'VFAT', 42 | 15 => 'alternate MVS', 43 | 16 => 'BeOS', 44 | 17 => 'Tandem', 45 | 18 => 'OS/400', 46 | self::OS_MAC_OSX => 'OS/X (Darwin)', 47 | 30 => 'AtheOS/Syllable', 48 | ]; 49 | 50 | public static function getPlatformName(int $platform): string 51 | { 52 | return self::PLATFORMS[$platform] ?? 'Unknown'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Constants/ZipVersion.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Constants; 13 | 14 | /** 15 | * Version needed to extract or software version. 16 | * 17 | * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT Section 4.4.3 18 | */ 19 | interface ZipVersion 20 | { 21 | /** @var int 1.0 - Default value */ 22 | public const v10_DEFAULT_MIN = 10; 23 | 24 | /** @var int 1.1 - File is a volume label */ 25 | public const v11_FILE_VOLUME_LABEL = 11; 26 | 27 | /** 28 | * 2.0 - File is a folder (directory) 29 | * 2.0 - File is compressed using Deflate compression 30 | * 2.0 - File is encrypted using traditional PKWARE encryption. 31 | * 32 | * @var int 33 | */ 34 | public const v20_DEFLATED_FOLDER_ZIPCRYPTO = 20; 35 | 36 | /** @var int 2.1 - File is compressed using Deflate64(tm) */ 37 | public const v21_DEFLATED64 = 21; 38 | 39 | /** @var int 2.5 - File is compressed using PKWARE DCL Implode */ 40 | public const v25_IMPLODED = 25; 41 | 42 | /** @var int 2.7 - File is a patch data set */ 43 | public const v27_PATCH_DATA = 27; 44 | 45 | /** @var int 4.5 - File uses ZIP64 format extensions */ 46 | public const v45_ZIP64_EXT = 45; 47 | 48 | /** @var int 4.6 - File is compressed using BZIP2 compression */ 49 | public const v46_BZIP2 = 46; 50 | 51 | /** 52 | * 5.0 - File is encrypted using DES 53 | * 5.0 - File is encrypted using 3DES 54 | * 5.0 - File is encrypted using original RC2 encryption 55 | * 5.0 - File is encrypted using RC4 encryption. 56 | * 57 | * @var int 58 | */ 59 | public const v50_ENCR_DES_3DES_RC2_ORIG_RC4 = 50; 60 | 61 | /** 62 | * 5.1 - File is encrypted using AES encryption 63 | * 5.1 - File is encrypted using corrected RC2 encryption**. 64 | * 65 | * @var int 66 | */ 67 | public const v51_ENCR_AES_RC2_CORRECT = 51; 68 | 69 | /** @var int 5.2 - File is encrypted using corrected RC2-64 encryption** */ 70 | public const v52_ENCR_RC2_64_CORRECT = 52; 71 | 72 | /** @var int 6.1 - File is encrypted using non-OAEP key wrapping*** */ 73 | public const v61_ENCR_NON_OAE_KEY_WRAP = 61; 74 | 75 | /** @var int 6.2 - Central directory encryption */ 76 | public const v62_ENCR_CENTRAL_DIR = 62; 77 | 78 | /** 79 | * 6.3 - File is compressed using LZMA 80 | * 6.3 - File is compressed using PPMd+ 81 | * 6.3 - File is encrypted using Blowfish 82 | * 6.3 - File is encrypted using Twofish. 83 | * 84 | * @var int 85 | */ 86 | public const v63_LZMA_PPMD_BLOWFISH_TWOFISH = 63; 87 | } 88 | -------------------------------------------------------------------------------- /src/Exception/Crc32Exception.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | /** 15 | * Thrown to indicate a CRC32 mismatch between the declared value in the 16 | * Central File Header and the Data Descriptor or between the declared value 17 | * and the computed value from the decompressed data. 18 | * 19 | * The exception detail message is the name of the ZIP entry. 20 | */ 21 | class Crc32Exception extends ZipException 22 | { 23 | /** Expected crc. */ 24 | private int $expectedCrc; 25 | 26 | /** Actual crc. */ 27 | private int $actualCrc; 28 | 29 | public function __construct(string $name, int $expected, int $actual) 30 | { 31 | parent::__construct( 32 | sprintf( 33 | '%s (expected CRC32 value 0x%x, but is actually 0x%x)', 34 | $name, 35 | $expected, 36 | $actual 37 | ) 38 | ); 39 | $this->expectedCrc = $expected; 40 | $this->actualCrc = $actual; 41 | } 42 | 43 | /** 44 | * Returns expected crc. 45 | */ 46 | public function getExpectedCrc(): int 47 | { 48 | return $this->expectedCrc; 49 | } 50 | 51 | /** 52 | * Returns actual crc. 53 | */ 54 | public function getActualCrc(): int 55 | { 56 | return $this->actualCrc; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | /** 15 | * Thrown to indicate that a method has been passed an illegal or 16 | * inappropriate argument. 17 | */ 18 | class InvalidArgumentException extends RuntimeException 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | /** 15 | * Runtime exception. 16 | * Exception thrown if an error which can only be found on runtime occurs. 17 | */ 18 | class RuntimeException extends \RuntimeException 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/ZipAuthenticationException.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | /** 15 | * Thrown to indicate that an authenticated ZIP entry has been tampered with. 16 | */ 17 | class ZipAuthenticationException extends ZipCryptoException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/Exception/ZipCryptoException.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | /** 15 | * Thrown if there is an issue when reading or writing an encrypted ZIP file 16 | * or entry. 17 | */ 18 | class ZipCryptoException extends ZipException 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/ZipEntryNotFoundException.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | use PhpZip\Model\ZipEntry; 15 | 16 | /** 17 | * Thrown if entry not found. 18 | */ 19 | class ZipEntryNotFoundException extends ZipException 20 | { 21 | private string $entryName; 22 | 23 | /** 24 | * @param ZipEntry|string $entryName 25 | */ 26 | public function __construct($entryName) 27 | { 28 | $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : $entryName; 29 | parent::__construct(sprintf( 30 | 'Zip Entry "%s" was not found in the archive.', 31 | $entryName 32 | )); 33 | $this->entryName = $entryName; 34 | } 35 | 36 | public function getEntryName(): string 37 | { 38 | return $this->entryName; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/ZipException.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | /** 15 | * Signals that a Zip exception of some sort has occurred. 16 | * 17 | * @see \Exception 18 | */ 19 | class ZipException extends \Exception 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/ZipUnsupportMethodException.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Exception; 13 | 14 | class ZipUnsupportMethodException extends ZipException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/IO/Filter/Cipher/Pkware/PKCryptContext.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Filter\Cipher\Pkware; 13 | 14 | use PhpZip\Exception\RuntimeException; 15 | use PhpZip\Exception\ZipAuthenticationException; 16 | use PhpZip\Util\MathUtil; 17 | 18 | /** 19 | * Traditional PKWARE Encryption Engine. 20 | * 21 | * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification 22 | */ 23 | class PKCryptContext 24 | { 25 | /** @var int Encryption header size */ 26 | public const STD_DEC_HDR_SIZE = 12; 27 | 28 | /** 29 | * Crc table. 30 | * 31 | * @var int[]|array 32 | */ 33 | private const CRC_TABLE = [ 34 | 0x00000000, 35 | 0x77073096, 36 | 0xEE0E612C, 37 | 0x990951BA, 38 | 0x076DC419, 39 | 0x706AF48F, 40 | 0xE963A535, 41 | 0x9E6495A3, 42 | 0x0EDB8832, 43 | 0x79DCB8A4, 44 | 0xE0D5E91E, 45 | 0x97D2D988, 46 | 0x09B64C2B, 47 | 0x7EB17CBD, 48 | 0xE7B82D07, 49 | 0x90BF1D91, 50 | 0x1DB71064, 51 | 0x6AB020F2, 52 | 0xF3B97148, 53 | 0x84BE41DE, 54 | 0x1ADAD47D, 55 | 0x6DDDE4EB, 56 | 0xF4D4B551, 57 | 0x83D385C7, 58 | 0x136C9856, 59 | 0x646BA8C0, 60 | 0xFD62F97A, 61 | 0x8A65C9EC, 62 | 0x14015C4F, 63 | 0x63066CD9, 64 | 0xFA0F3D63, 65 | 0x8D080DF5, 66 | 0x3B6E20C8, 67 | 0x4C69105E, 68 | 0xD56041E4, 69 | 0xA2677172, 70 | 0x3C03E4D1, 71 | 0x4B04D447, 72 | 0xD20D85FD, 73 | 0xA50AB56B, 74 | 0x35B5A8FA, 75 | 0x42B2986C, 76 | 0xDBBBC9D6, 77 | 0xACBCF940, 78 | 0x32D86CE3, 79 | 0x45DF5C75, 80 | 0xDCD60DCF, 81 | 0xABD13D59, 82 | 0x26D930AC, 83 | 0x51DE003A, 84 | 0xC8D75180, 85 | 0xBFD06116, 86 | 0x21B4F4B5, 87 | 0x56B3C423, 88 | 0xCFBA9599, 89 | 0xB8BDA50F, 90 | 0x2802B89E, 91 | 0x5F058808, 92 | 0xC60CD9B2, 93 | 0xB10BE924, 94 | 0x2F6F7C87, 95 | 0x58684C11, 96 | 0xC1611DAB, 97 | 0xB6662D3D, 98 | 0x76DC4190, 99 | 0x01DB7106, 100 | 0x98D220BC, 101 | 0xEFD5102A, 102 | 0x71B18589, 103 | 0x06B6B51F, 104 | 0x9FBFE4A5, 105 | 0xE8B8D433, 106 | 0x7807C9A2, 107 | 0x0F00F934, 108 | 0x9609A88E, 109 | 0xE10E9818, 110 | 0x7F6A0DBB, 111 | 0x086D3D2D, 112 | 0x91646C97, 113 | 0xE6635C01, 114 | 0x6B6B51F4, 115 | 0x1C6C6162, 116 | 0x856530D8, 117 | 0xF262004E, 118 | 0x6C0695ED, 119 | 0x1B01A57B, 120 | 0x8208F4C1, 121 | 0xF50FC457, 122 | 0x65B0D9C6, 123 | 0x12B7E950, 124 | 0x8BBEB8EA, 125 | 0xFCB9887C, 126 | 0x62DD1DDF, 127 | 0x15DA2D49, 128 | 0x8CD37CF3, 129 | 0xFBD44C65, 130 | 0x4DB26158, 131 | 0x3AB551CE, 132 | 0xA3BC0074, 133 | 0xD4BB30E2, 134 | 0x4ADFA541, 135 | 0x3DD895D7, 136 | 0xA4D1C46D, 137 | 0xD3D6F4FB, 138 | 0x4369E96A, 139 | 0x346ED9FC, 140 | 0xAD678846, 141 | 0xDA60B8D0, 142 | 0x44042D73, 143 | 0x33031DE5, 144 | 0xAA0A4C5F, 145 | 0xDD0D7CC9, 146 | 0x5005713C, 147 | 0x270241AA, 148 | 0xBE0B1010, 149 | 0xC90C2086, 150 | 0x5768B525, 151 | 0x206F85B3, 152 | 0xB966D409, 153 | 0xCE61E49F, 154 | 0x5EDEF90E, 155 | 0x29D9C998, 156 | 0xB0D09822, 157 | 0xC7D7A8B4, 158 | 0x59B33D17, 159 | 0x2EB40D81, 160 | 0xB7BD5C3B, 161 | 0xC0BA6CAD, 162 | 0xEDB88320, 163 | 0x9ABFB3B6, 164 | 0x03B6E20C, 165 | 0x74B1D29A, 166 | 0xEAD54739, 167 | 0x9DD277AF, 168 | 0x04DB2615, 169 | 0x73DC1683, 170 | 0xE3630B12, 171 | 0x94643B84, 172 | 0x0D6D6A3E, 173 | 0x7A6A5AA8, 174 | 0xE40ECF0B, 175 | 0x9309FF9D, 176 | 0x0A00AE27, 177 | 0x7D079EB1, 178 | 0xF00F9344, 179 | 0x8708A3D2, 180 | 0x1E01F268, 181 | 0x6906C2FE, 182 | 0xF762575D, 183 | 0x806567CB, 184 | 0x196C3671, 185 | 0x6E6B06E7, 186 | 0xFED41B76, 187 | 0x89D32BE0, 188 | 0x10DA7A5A, 189 | 0x67DD4ACC, 190 | 0xF9B9DF6F, 191 | 0x8EBEEFF9, 192 | 0x17B7BE43, 193 | 0x60B08ED5, 194 | 0xD6D6A3E8, 195 | 0xA1D1937E, 196 | 0x38D8C2C4, 197 | 0x4FDFF252, 198 | 0xD1BB67F1, 199 | 0xA6BC5767, 200 | 0x3FB506DD, 201 | 0x48B2364B, 202 | 0xD80D2BDA, 203 | 0xAF0A1B4C, 204 | 0x36034AF6, 205 | 0x41047A60, 206 | 0xDF60EFC3, 207 | 0xA867DF55, 208 | 0x316E8EEF, 209 | 0x4669BE79, 210 | 0xCB61B38C, 211 | 0xBC66831A, 212 | 0x256FD2A0, 213 | 0x5268E236, 214 | 0xCC0C7795, 215 | 0xBB0B4703, 216 | 0x220216B9, 217 | 0x5505262F, 218 | 0xC5BA3BBE, 219 | 0xB2BD0B28, 220 | 0x2BB45A92, 221 | 0x5CB36A04, 222 | 0xC2D7FFA7, 223 | 0xB5D0CF31, 224 | 0x2CD99E8B, 225 | 0x5BDEAE1D, 226 | 0x9B64C2B0, 227 | 0xEC63F226, 228 | 0x756AA39C, 229 | 0x026D930A, 230 | 0x9C0906A9, 231 | 0xEB0E363F, 232 | 0x72076785, 233 | 0x05005713, 234 | 0x95BF4A82, 235 | 0xE2B87A14, 236 | 0x7BB12BAE, 237 | 0x0CB61B38, 238 | 0x92D28E9B, 239 | 0xE5D5BE0D, 240 | 0x7CDCEFB7, 241 | 0x0BDBDF21, 242 | 0x86D3D2D4, 243 | 0xF1D4E242, 244 | 0x68DDB3F8, 245 | 0x1FDA836E, 246 | 0x81BE16CD, 247 | 0xF6B9265B, 248 | 0x6FB077E1, 249 | 0x18B74777, 250 | 0x88085AE6, 251 | 0xFF0F6A70, 252 | 0x66063BCA, 253 | 0x11010B5C, 254 | 0x8F659EFF, 255 | 0xF862AE69, 256 | 0x616BFFD3, 257 | 0x166CCF45, 258 | 0xA00AE278, 259 | 0xD70DD2EE, 260 | 0x4E048354, 261 | 0x3903B3C2, 262 | 0xA7672661, 263 | 0xD06016F7, 264 | 0x4969474D, 265 | 0x3E6E77DB, 266 | 0xAED16A4A, 267 | 0xD9D65ADC, 268 | 0x40DF0B66, 269 | 0x37D83BF0, 270 | 0xA9BCAE53, 271 | 0xDEBB9EC5, 272 | 0x47B2CF7F, 273 | 0x30B5FFE9, 274 | 0xBDBDF21C, 275 | 0xCABAC28A, 276 | 0x53B39330, 277 | 0x24B4A3A6, 278 | 0xBAD03605, 279 | 0xCDD70693, 280 | 0x54DE5729, 281 | 0x23D967BF, 282 | 0xB3667A2E, 283 | 0xC4614AB8, 284 | 0x5D681B02, 285 | 0x2A6F2B94, 286 | 0xB40BBE37, 287 | 0xC30C8EA1, 288 | 0x5A05DF1B, 289 | 0x2D02EF8D, 290 | ]; 291 | 292 | /** @var array encryption keys */ 293 | private array $keys; 294 | 295 | public function __construct(string $password) 296 | { 297 | if (\PHP_INT_SIZE === 4) { 298 | throw new RuntimeException('Traditional PKWARE Encryption is not supported in 32-bit PHP.'); 299 | } 300 | 301 | $this->keys = [ 302 | 305419896, 303 | 591751049, 304 | 878082192, 305 | ]; 306 | 307 | foreach (unpack('C*', $password) as $byte) { 308 | $this->updateKeys($byte); 309 | } 310 | } 311 | 312 | /** 313 | * @throws ZipAuthenticationException 314 | */ 315 | public function checkHeader(string $header, int $checkByte): void 316 | { 317 | $byte = 0; 318 | 319 | foreach (unpack('C*', $header) as $byte) { 320 | $byte = ($byte ^ $this->decryptByte()) & 0xFF; 321 | $this->updateKeys($byte); 322 | } 323 | 324 | if ($byte !== $checkByte) { 325 | throw new ZipAuthenticationException('Invalid password'); 326 | } 327 | } 328 | 329 | public function decryptString(string $content): string 330 | { 331 | $decryptContent = ''; 332 | 333 | foreach (unpack('C*', $content) as $byte) { 334 | $byte = ($byte ^ $this->decryptByte()) & 0xFF; 335 | $this->updateKeys($byte); 336 | $decryptContent .= \chr($byte); 337 | } 338 | 339 | return $decryptContent; 340 | } 341 | 342 | /** 343 | * Decrypt byte. 344 | */ 345 | private function decryptByte(): int 346 | { 347 | $temp = $this->keys[2] | 2; 348 | 349 | return (($temp * ($temp ^ 1)) >> 8) & 0xFFFFFF; 350 | } 351 | 352 | /** 353 | * Update keys. 354 | */ 355 | private function updateKeys(int $charAt): void 356 | { 357 | $this->keys[0] = $this->crc32($this->keys[0], $charAt); 358 | $this->keys[1] += ($this->keys[0] & 0xFF); 359 | $this->keys[1] = MathUtil::toSignedInt32($this->keys[1] * 134775813 + 1); 360 | $this->keys[2] = MathUtil::toSignedInt32($this->crc32($this->keys[2], ($this->keys[1] >> 24) & 0xFF)); 361 | } 362 | 363 | /** 364 | * Update crc. 365 | */ 366 | private function crc32(int $oldCrc, int $charAt): int 367 | { 368 | return (($oldCrc >> 8) & 0xFFFFFF) ^ self::CRC_TABLE[($oldCrc ^ $charAt) & 0xFF]; 369 | } 370 | 371 | public function encryptString(string $content): string 372 | { 373 | $encryptContent = ''; 374 | 375 | foreach (unpack('C*', $content) as $val) { 376 | $encryptContent .= pack('c', $this->encryptByte($val)); 377 | } 378 | 379 | return $encryptContent; 380 | } 381 | 382 | private function encryptByte(int $byte): int 383 | { 384 | $tempVal = $byte ^ $this->decryptByte() & 0xFF; 385 | $this->updateKeys($byte); 386 | 387 | return $tempVal; 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/IO/Filter/Cipher/Pkware/PKDecryptionStreamFilter.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Filter\Cipher\Pkware; 13 | 14 | use PhpZip\Exception\ZipAuthenticationException; 15 | use PhpZip\Model\ZipEntry; 16 | 17 | /** 18 | * Decryption PKWARE Traditional Encryption. 19 | */ 20 | class PKDecryptionStreamFilter extends \php_user_filter 21 | { 22 | public const FILTER_NAME = 'phpzip.decryption.pkware'; 23 | 24 | private int $checkByte = 0; 25 | 26 | private int $readLength = 0; 27 | 28 | private int $size = 0; 29 | 30 | private bool $readHeader = false; 31 | 32 | private PKCryptContext $context; 33 | 34 | public static function register(): bool 35 | { 36 | return stream_filter_register(self::FILTER_NAME, __CLASS__); 37 | } 38 | 39 | /** 40 | * @see https://php.net/manual/en/php-user-filter.oncreate.php 41 | */ 42 | public function onCreate(): bool 43 | { 44 | if (!isset($this->params['entry'])) { 45 | return false; 46 | } 47 | 48 | if (!($this->params['entry'] instanceof ZipEntry)) { 49 | throw new \RuntimeException('ZipEntry expected'); 50 | } 51 | /** @var ZipEntry $entry */ 52 | $entry = $this->params['entry']; 53 | $password = $entry->getPassword(); 54 | 55 | if ($password === null) { 56 | return false; 57 | } 58 | 59 | $this->size = $entry->getCompressedSize(); 60 | 61 | // init context 62 | $this->context = new PKCryptContext($password); 63 | 64 | // init check byte 65 | if ($entry->isDataDescriptorEnabled()) { 66 | $this->checkByte = ($entry->getDosTime() >> 8) & 0xFF; 67 | } else { 68 | $this->checkByte = ($entry->getCrc() >> 24) & 0xFF; 69 | } 70 | 71 | $this->readLength = 0; 72 | $this->readHeader = false; 73 | 74 | return true; 75 | } 76 | 77 | /** 78 | * Decryption filter. 79 | * 80 | * @todo USE FFI in php 7.4 81 | * @noinspection PhpDocSignatureInspection 82 | * 83 | * @param mixed $in 84 | * @param mixed $out 85 | * @param mixed $consumed 86 | * @param mixed $closing 87 | * 88 | * @throws ZipAuthenticationException 89 | */ 90 | public function filter($in, $out, &$consumed, $closing): int 91 | { 92 | while ($bucket = stream_bucket_make_writeable($in)) { 93 | $buffer = $bucket->data; 94 | $this->readLength += $bucket->datalen; 95 | 96 | if ($this->readLength > $this->size) { 97 | $buffer = substr($buffer, 0, $this->size - $this->readLength); 98 | } 99 | 100 | if (!$this->readHeader) { 101 | $header = substr($buffer, 0, PKCryptContext::STD_DEC_HDR_SIZE); 102 | $this->context->checkHeader($header, $this->checkByte); 103 | 104 | $buffer = substr($buffer, PKCryptContext::STD_DEC_HDR_SIZE); 105 | $this->readHeader = true; 106 | } 107 | 108 | $bucket->data = $this->context->decryptString($buffer); 109 | 110 | $consumed += $bucket->datalen; 111 | stream_bucket_append($out, $bucket); 112 | } 113 | 114 | return \PSFS_PASS_ON; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/IO/Filter/Cipher/Pkware/PKEncryptionStreamFilter.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Filter\Cipher\Pkware; 13 | 14 | use PhpZip\Exception\RuntimeException; 15 | use PhpZip\Model\ZipEntry; 16 | 17 | /** 18 | * Encryption PKWARE Traditional Encryption. 19 | */ 20 | class PKEncryptionStreamFilter extends \php_user_filter 21 | { 22 | public const FILTER_NAME = 'phpzip.encryption.pkware'; 23 | 24 | private int $size; 25 | 26 | private string $headerBytes; 27 | 28 | private int $writeLength; 29 | 30 | private bool $writeHeader; 31 | 32 | private PKCryptContext $context; 33 | 34 | public static function register(): bool 35 | { 36 | return stream_filter_register(self::FILTER_NAME, __CLASS__); 37 | } 38 | 39 | /** 40 | * @see https://php.net/manual/en/php-user-filter.oncreate.php 41 | */ 42 | public function onCreate(): bool 43 | { 44 | if (\PHP_INT_SIZE === 4) { 45 | throw new RuntimeException('Traditional PKWARE Encryption is not supported in 32-bit PHP.'); 46 | } 47 | 48 | if (!isset($this->params['entry'], $this->params['size'])) { 49 | return false; 50 | } 51 | 52 | if (!($this->params['entry'] instanceof ZipEntry)) { 53 | throw new \RuntimeException('ZipEntry expected'); 54 | } 55 | /** @var ZipEntry $entry */ 56 | $entry = $this->params['entry']; 57 | $password = $entry->getPassword(); 58 | 59 | if ($password === null) { 60 | return false; 61 | } 62 | 63 | $this->size = (int) $this->params['size']; 64 | 65 | // init keys 66 | $this->context = new PKCryptContext($password); 67 | 68 | $crc = $entry->isDataDescriptorRequired() || $entry->getCrc() === ZipEntry::UNKNOWN 69 | ? ($entry->getDosTime() & 0x0000FFFF) << 16 70 | : $entry->getCrc(); 71 | 72 | try { 73 | $headerBytes = random_bytes(PKCryptContext::STD_DEC_HDR_SIZE); 74 | } catch (\Exception $e) { 75 | throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e); 76 | } 77 | 78 | $headerBytes[PKCryptContext::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xFF); 79 | $headerBytes[PKCryptContext::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xFF); 80 | 81 | $this->headerBytes = $headerBytes; 82 | $this->writeLength = 0; 83 | $this->writeHeader = false; 84 | 85 | return true; 86 | } 87 | 88 | /** 89 | * Encryption filter. 90 | * 91 | * @todo USE FFI in php 7.4 92 | * 93 | * @noinspection PhpDocSignatureInspection 94 | * 95 | * @param mixed $in 96 | * @param mixed $out 97 | * @param mixed $consumed 98 | * @param mixed $closing 99 | */ 100 | public function filter($in, $out, &$consumed, $closing): int 101 | { 102 | while ($bucket = stream_bucket_make_writeable($in)) { 103 | $buffer = $bucket->data; 104 | $this->writeLength += $bucket->datalen; 105 | 106 | if ($this->writeLength > $this->size) { 107 | $buffer = substr($buffer, 0, $this->size - $this->writeLength); 108 | } 109 | 110 | $data = ''; 111 | 112 | if (!$this->writeHeader) { 113 | $data .= $this->context->encryptString($this->headerBytes); 114 | $this->writeHeader = true; 115 | } 116 | 117 | $data .= $this->context->encryptString($buffer); 118 | 119 | $bucket->data = $data; 120 | 121 | $consumed += $bucket->datalen; 122 | stream_bucket_append($out, $bucket); 123 | } 124 | 125 | return \PSFS_PASS_ON; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/IO/Filter/Cipher/WinZipAes/WinZipAesContext.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Filter\Cipher\WinZipAes; 13 | 14 | use PhpZip\Exception\RuntimeException; 15 | use PhpZip\Exception\ZipAuthenticationException; 16 | use PhpZip\Util\CryptoUtil; 17 | 18 | /** 19 | * WinZip Aes Encryption. 20 | * 21 | * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT APPENDIX E 22 | * @see https://www.winzip.com/win/en/aes_info.html 23 | * 24 | * @internal 25 | */ 26 | class WinZipAesContext 27 | { 28 | /** @var int AES Block size */ 29 | public const BLOCK_SIZE = self::IV_SIZE; 30 | 31 | /** @var int Footer size */ 32 | public const FOOTER_SIZE = 10; 33 | 34 | /** @var int The iteration count for the derived keys of the cipher, KLAC and MAC. */ 35 | public const ITERATION_COUNT = 1000; 36 | 37 | /** @var int Password verifier size */ 38 | public const PASSWORD_VERIFIER_SIZE = 2; 39 | 40 | /** @var int IV size */ 41 | public const IV_SIZE = 16; 42 | 43 | private string $iv; 44 | 45 | private string $key; 46 | 47 | private \HashContext $hmacContext; 48 | 49 | private string $passwordVerifier; 50 | 51 | public function __construct(int $encryptionStrengthBits, string $password, string $salt) 52 | { 53 | if ($password === '') { 54 | throw new RuntimeException('$password is empty'); 55 | } 56 | 57 | if (empty($salt)) { 58 | throw new RuntimeException('$salt is empty'); 59 | } 60 | 61 | // WinZip 99-character limit https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ 62 | $password = substr($password, 0, 99); 63 | 64 | $this->iv = str_repeat("\0", self::IV_SIZE); 65 | $keyStrengthBytes = (int) ($encryptionStrengthBits / 8); 66 | $hashLength = $keyStrengthBytes * 2 + self::PASSWORD_VERIFIER_SIZE * 8; 67 | 68 | $hash = hash_pbkdf2( 69 | 'sha1', 70 | $password, 71 | $salt, 72 | self::ITERATION_COUNT, 73 | $hashLength, 74 | true 75 | ); 76 | 77 | $this->key = substr($hash, 0, $keyStrengthBytes); 78 | $sha1Mac = substr($hash, $keyStrengthBytes, $keyStrengthBytes); 79 | $this->hmacContext = hash_init('sha1', \HASH_HMAC, $sha1Mac); 80 | $this->passwordVerifier = substr($hash, 2 * $keyStrengthBytes, self::PASSWORD_VERIFIER_SIZE); 81 | } 82 | 83 | public function getPasswordVerifier(): string 84 | { 85 | return $this->passwordVerifier; 86 | } 87 | 88 | public function updateIv(): void 89 | { 90 | for ($ivCharIndex = 0; $ivCharIndex < self::IV_SIZE; $ivCharIndex++) { 91 | $ivByte = \ord($this->iv[$ivCharIndex]); 92 | 93 | if (++$ivByte === 256) { 94 | // overflow, set this one to 0, increment next 95 | $this->iv[$ivCharIndex] = "\0"; 96 | } else { 97 | // no overflow, just write incremented number back and abort 98 | $this->iv[$ivCharIndex] = \chr($ivByte); 99 | 100 | break; 101 | } 102 | } 103 | } 104 | 105 | public function decryption(string $data): string 106 | { 107 | hash_update($this->hmacContext, $data); 108 | 109 | return CryptoUtil::decryptAesCtr($data, $this->key, $this->iv); 110 | } 111 | 112 | public function encrypt(string $data): string 113 | { 114 | $encryptionData = CryptoUtil::encryptAesCtr($data, $this->key, $this->iv); 115 | hash_update($this->hmacContext, $encryptionData); 116 | 117 | return $encryptionData; 118 | } 119 | 120 | /** 121 | * @throws ZipAuthenticationException 122 | */ 123 | public function checkAuthCode(string $authCode): void 124 | { 125 | $hmac = $this->getHmac(); 126 | 127 | // check authenticationCode 128 | if (strcmp($hmac, $authCode) !== 0) { 129 | throw new ZipAuthenticationException('Authenticated WinZip AES entry content has been tampered with.'); 130 | } 131 | } 132 | 133 | public function getHmac(): string 134 | { 135 | return substr( 136 | hash_final($this->hmacContext, true), 137 | 0, 138 | self::FOOTER_SIZE 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/IO/Filter/Cipher/WinZipAes/WinZipAesDecryptionStreamFilter.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Filter\Cipher\WinZipAes; 13 | 14 | use PhpZip\Exception\RuntimeException; 15 | use PhpZip\Exception\ZipAuthenticationException; 16 | use PhpZip\Model\Extra\Fields\WinZipAesExtraField; 17 | use PhpZip\Model\ZipEntry; 18 | 19 | /** 20 | * Decrypt WinZip AES stream. 21 | */ 22 | class WinZipAesDecryptionStreamFilter extends \php_user_filter 23 | { 24 | public const FILTER_NAME = 'phpzip.decryption.winzipaes'; 25 | 26 | private string $buffer; 27 | 28 | private ?string $authenticationCode = null; 29 | 30 | private int $encBlockPosition = 0; 31 | 32 | private int $encBlockLength = 0; 33 | 34 | private int $readLength = 0; 35 | 36 | private ZipEntry $entry; 37 | 38 | private ?WinZipAesContext $context = null; 39 | 40 | public static function register(): bool 41 | { 42 | return stream_filter_register(self::FILTER_NAME, __CLASS__); 43 | } 44 | 45 | /** 46 | * @noinspection DuplicatedCode 47 | */ 48 | public function onCreate(): bool 49 | { 50 | if (!isset($this->params['entry'])) { 51 | return false; 52 | } 53 | 54 | if (!($this->params['entry'] instanceof ZipEntry)) { 55 | throw new \RuntimeException('ZipEntry expected'); 56 | } 57 | $this->entry = $this->params['entry']; 58 | 59 | if ( 60 | $this->entry->getPassword() === null 61 | || !$this->entry->isEncrypted() 62 | || !$this->entry->hasExtraField(WinZipAesExtraField::HEADER_ID) 63 | ) { 64 | return false; 65 | } 66 | 67 | $this->buffer = ''; 68 | 69 | return true; 70 | } 71 | 72 | /** 73 | * @noinspection PhpDocSignatureInspection 74 | * 75 | * @param mixed $in 76 | * @param mixed $out 77 | * @param mixed $consumed 78 | * @param mixed $closing 79 | * 80 | * @throws ZipAuthenticationException 81 | */ 82 | public function filter($in, $out, &$consumed, $closing): int 83 | { 84 | while ($bucket = stream_bucket_make_writeable($in)) { 85 | $this->buffer .= $bucket->data; 86 | $this->readLength += $bucket->datalen; 87 | 88 | if ($this->readLength > $this->entry->getCompressedSize()) { 89 | $this->buffer = substr($this->buffer, 0, $this->entry->getCompressedSize() - $this->readLength); 90 | } 91 | 92 | // read header 93 | if ($this->context === null) { 94 | /** 95 | * @var WinZipAesExtraField|null $winZipExtra 96 | */ 97 | $winZipExtra = $this->entry->getExtraField(WinZipAesExtraField::HEADER_ID); 98 | 99 | if ($winZipExtra === null) { 100 | throw new RuntimeException('$winZipExtra is null'); 101 | } 102 | $saltSize = $winZipExtra->getSaltSize(); 103 | $headerSize = $saltSize + WinZipAesContext::PASSWORD_VERIFIER_SIZE; 104 | 105 | if (\strlen($this->buffer) < $headerSize) { 106 | return \PSFS_FEED_ME; 107 | } 108 | 109 | $salt = substr($this->buffer, 0, $saltSize); 110 | $passwordVerifier = substr($this->buffer, $saltSize, WinZipAesContext::PASSWORD_VERIFIER_SIZE); 111 | $password = $this->entry->getPassword(); 112 | 113 | if ($password === null) { 114 | throw new RuntimeException('$password is null'); 115 | } 116 | $this->context = new WinZipAesContext($winZipExtra->getEncryptionStrength(), $password, $salt); 117 | unset($password); 118 | 119 | // Verify password. 120 | if ($passwordVerifier !== $this->context->getPasswordVerifier()) { 121 | throw new ZipAuthenticationException('Invalid password'); 122 | } 123 | 124 | $this->encBlockPosition = 0; 125 | $this->encBlockLength = $this->entry->getCompressedSize() - $headerSize - WinZipAesContext::FOOTER_SIZE; 126 | 127 | $this->buffer = substr($this->buffer, $headerSize); 128 | } 129 | 130 | // encrypt data 131 | $plainText = ''; 132 | $offset = 0; 133 | $len = \strlen($this->buffer); 134 | $remaining = $this->encBlockLength - $this->encBlockPosition; 135 | 136 | if ($remaining >= WinZipAesContext::BLOCK_SIZE && $len < WinZipAesContext::BLOCK_SIZE) { 137 | return \PSFS_FEED_ME; 138 | } 139 | $limit = min($len, $remaining); 140 | 141 | if ($remaining > $limit && ($limit % WinZipAesContext::BLOCK_SIZE) !== 0) { 142 | $limit -= ($limit % WinZipAesContext::BLOCK_SIZE); 143 | } 144 | 145 | while ($offset < $limit) { 146 | $this->context->updateIv(); 147 | $length = min(WinZipAesContext::BLOCK_SIZE, $limit - $offset); 148 | $data = substr($this->buffer, 0, $length); 149 | $plainText .= $this->context->decryption($data); 150 | $offset += $length; 151 | $this->buffer = substr($this->buffer, $length); 152 | } 153 | $this->encBlockPosition += $offset; 154 | 155 | if ( 156 | $this->encBlockPosition === $this->encBlockLength 157 | && \strlen($this->buffer) === WinZipAesContext::FOOTER_SIZE 158 | ) { 159 | $this->authenticationCode = $this->buffer; 160 | $this->buffer = ''; 161 | } 162 | 163 | $bucket->data = $plainText; 164 | $consumed += $bucket->datalen; 165 | stream_bucket_append($out, $bucket); 166 | } 167 | 168 | return \PSFS_PASS_ON; 169 | } 170 | 171 | /** 172 | * @see http://php.net/manual/en/php-user-filter.onclose.php 173 | * 174 | * @throws ZipAuthenticationException 175 | */ 176 | public function onClose(): void 177 | { 178 | $this->buffer = ''; 179 | 180 | if ($this->context !== null && $this->authenticationCode !== null) { 181 | $this->context->checkAuthCode($this->authenticationCode); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/IO/Filter/Cipher/WinZipAes/WinZipAesEncryptionStreamFilter.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Filter\Cipher\WinZipAes; 13 | 14 | use PhpZip\Exception\RuntimeException; 15 | use PhpZip\Model\Extra\Fields\WinZipAesExtraField; 16 | use PhpZip\Model\ZipEntry; 17 | 18 | /** 19 | * Encrypt WinZip AES stream. 20 | */ 21 | class WinZipAesEncryptionStreamFilter extends \php_user_filter 22 | { 23 | public const FILTER_NAME = 'phpzip.encryption.winzipaes'; 24 | 25 | private string $buffer; 26 | 27 | private int $remaining = 0; 28 | 29 | private ZipEntry $entry; 30 | 31 | private int $size; 32 | 33 | private ?WinZipAesContext $context = null; 34 | 35 | public static function register(): bool 36 | { 37 | return stream_filter_register(self::FILTER_NAME, __CLASS__); 38 | } 39 | 40 | /** 41 | * @noinspection DuplicatedCode 42 | */ 43 | public function onCreate(): bool 44 | { 45 | if (!isset($this->params['entry'])) { 46 | return false; 47 | } 48 | 49 | if (!($this->params['entry'] instanceof ZipEntry)) { 50 | throw new \RuntimeException('ZipEntry expected'); 51 | } 52 | $this->entry = $this->params['entry']; 53 | 54 | if ( 55 | $this->entry->getPassword() === null 56 | || !$this->entry->isEncrypted() 57 | || !$this->entry->hasExtraField(WinZipAesExtraField::HEADER_ID) 58 | ) { 59 | return false; 60 | } 61 | 62 | $this->size = (int) $this->params['size']; 63 | $this->context = null; 64 | $this->buffer = ''; 65 | 66 | return true; 67 | } 68 | 69 | public function filter($in, $out, &$consumed, $closing): int 70 | { 71 | while ($bucket = stream_bucket_make_writeable($in)) { 72 | $this->buffer .= $bucket->data; 73 | $this->remaining += $bucket->datalen; 74 | 75 | if ($this->remaining > $this->size) { 76 | $this->buffer = substr($this->buffer, 0, $this->size - $this->remaining); 77 | $this->remaining = $this->size; 78 | } 79 | 80 | $encryptionText = ''; 81 | 82 | // write header 83 | if ($this->context === null) { 84 | /** 85 | * @var WinZipAesExtraField|null $winZipExtra 86 | */ 87 | $winZipExtra = $this->entry->getExtraField(WinZipAesExtraField::HEADER_ID); 88 | 89 | if ($winZipExtra === null) { 90 | throw new RuntimeException('$winZipExtra is null'); 91 | } 92 | /** @psalm-var positive-int $saltSize */ 93 | $saltSize = $winZipExtra->getSaltSize(); 94 | 95 | try { 96 | $salt = random_bytes($saltSize); 97 | } catch (\Exception $e) { 98 | throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e); 99 | } 100 | $password = $this->entry->getPassword(); 101 | 102 | if ($password === null) { 103 | throw new RuntimeException('$password is null'); 104 | } 105 | $this->context = new WinZipAesContext( 106 | $winZipExtra->getEncryptionStrength(), 107 | $password, 108 | $salt 109 | ); 110 | 111 | $encryptionText .= $salt . $this->context->getPasswordVerifier(); 112 | } 113 | 114 | // encrypt data 115 | $offset = 0; 116 | $len = \strlen($this->buffer); 117 | $remaining = $this->remaining - $this->size; 118 | 119 | if ($remaining >= WinZipAesContext::BLOCK_SIZE && $len < WinZipAesContext::BLOCK_SIZE) { 120 | return \PSFS_FEED_ME; 121 | } 122 | $limit = max($len, $remaining); 123 | 124 | if ($remaining > $limit && ($limit % WinZipAesContext::BLOCK_SIZE) !== 0) { 125 | $limit -= ($limit % WinZipAesContext::BLOCK_SIZE); 126 | } 127 | 128 | while ($offset < $limit) { 129 | $this->context->updateIv(); 130 | $length = min(WinZipAesContext::BLOCK_SIZE, $limit - $offset); 131 | $encryptionText .= $this->context->encrypt( 132 | substr($this->buffer, 0, $length) 133 | ); 134 | $offset += $length; 135 | $this->buffer = substr($this->buffer, $length); 136 | } 137 | 138 | if ($remaining === 0) { 139 | $encryptionText .= $this->context->getHmac(); 140 | } 141 | 142 | $bucket->data = $encryptionText; 143 | $consumed += $bucket->datalen; 144 | 145 | stream_bucket_append($out, $bucket); 146 | } 147 | 148 | return \PSFS_PASS_ON; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/IO/Stream/ResponseStream.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Stream; 13 | 14 | use Psr\Http\Message\StreamInterface; 15 | 16 | /** 17 | * Implement PSR Message Stream. 18 | */ 19 | class ResponseStream implements StreamInterface 20 | { 21 | /** @var array */ 22 | private const READ_WRITE_MAP = [ 23 | 'read' => [ 24 | 'r' => true, 25 | 'w+' => true, 26 | 'r+' => true, 27 | 'x+' => true, 28 | 'c+' => true, 29 | 'rb' => true, 30 | 'w+b' => true, 31 | 'r+b' => true, 32 | 'x+b' => true, 33 | 'c+b' => true, 34 | 'rt' => true, 35 | 'w+t' => true, 36 | 'r+t' => true, 37 | 'x+t' => true, 38 | 'c+t' => true, 39 | 'a+' => true, 40 | ], 41 | 'write' => [ 42 | 'w' => true, 43 | 'w+' => true, 44 | 'rw' => true, 45 | 'r+' => true, 46 | 'x+' => true, 47 | 'c+' => true, 48 | 'wb' => true, 49 | 'w+b' => true, 50 | 'r+b' => true, 51 | 'x+b' => true, 52 | 'c+b' => true, 53 | 'w+t' => true, 54 | 'r+t' => true, 55 | 'x+t' => true, 56 | 'c+t' => true, 57 | 'a' => true, 58 | 'a+' => true, 59 | ], 60 | ]; 61 | 62 | /** @var resource|null */ 63 | private $stream; 64 | 65 | private ?int $size = null; 66 | 67 | private bool $seekable; 68 | 69 | private bool $readable; 70 | 71 | private bool $writable; 72 | 73 | private ?string $uri; 74 | 75 | /** 76 | * @param resource $stream stream resource to wrap 77 | * 78 | * @throws \InvalidArgumentException if the stream is not a stream resource 79 | */ 80 | public function __construct($stream) 81 | { 82 | if (!\is_resource($stream)) { 83 | throw new \InvalidArgumentException('Stream must be a resource'); 84 | } 85 | $this->stream = $stream; 86 | $meta = stream_get_meta_data($this->stream); 87 | $this->seekable = $meta['seekable']; 88 | $this->readable = isset(self::READ_WRITE_MAP['read'][$meta['mode']]); 89 | $this->writable = isset(self::READ_WRITE_MAP['write'][$meta['mode']]); 90 | $this->uri = $this->getMetadata('uri'); 91 | } 92 | 93 | /** 94 | * {@inheritDoc} 95 | * 96 | * @noinspection PhpMissingReturnTypeInspection 97 | */ 98 | public function getMetadata($key = null) 99 | { 100 | if ($this->stream === null) { 101 | return $key ? null : []; 102 | } 103 | $meta = stream_get_meta_data($this->stream); 104 | 105 | return $meta[$key] ?? null; 106 | } 107 | 108 | /** 109 | * Reads all data from the stream into a string, from the beginning to end. 110 | * 111 | * This method MUST attempt to seek to the beginning of the stream before 112 | * reading data and read the stream until the end is reached. 113 | * 114 | * Warning: This could attempt to load a large amount of data into memory. 115 | * 116 | * This method MUST NOT raise an exception in order to conform with PHP's 117 | * string casting operations. 118 | * 119 | * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring 120 | */ 121 | public function __toString(): string 122 | { 123 | if (!$this->stream) { 124 | return ''; 125 | } 126 | $this->rewind(); 127 | 128 | return (string) stream_get_contents($this->stream); 129 | } 130 | 131 | /** 132 | * Seek to the beginning of the stream. 133 | * 134 | * If the stream is not seekable, this method will raise an exception; 135 | * otherwise, it will perform a seek(0). 136 | * 137 | * @throws \RuntimeException on failure 138 | * 139 | * @see http://www.php.net/manual/en/function.fseek.php 140 | * @see seek() 141 | */ 142 | public function rewind(): void 143 | { 144 | $this->stream !== null && $this->seekable && rewind($this->stream); 145 | } 146 | 147 | /** 148 | * Get the size of the stream if known. 149 | * 150 | * @return int|null returns the size in bytes if known, or null if unknown 151 | */ 152 | public function getSize(): ?int 153 | { 154 | if ($this->size !== null) { 155 | return $this->size; 156 | } 157 | 158 | if (!$this->stream) { 159 | return null; 160 | } 161 | // Clear the stat cache if the stream has a URI 162 | if ($this->uri !== null) { 163 | clearstatcache(true, $this->uri); 164 | } 165 | $stats = fstat($this->stream); 166 | 167 | if (isset($stats['size'])) { 168 | $this->size = $stats['size']; 169 | 170 | return $this->size; 171 | } 172 | 173 | return null; 174 | } 175 | 176 | public function tell() 177 | { 178 | return $this->stream ? ftell($this->stream) : false; 179 | } 180 | 181 | /** 182 | * Returns true if the stream is at the end of the stream. 183 | */ 184 | public function eof(): bool 185 | { 186 | return !$this->stream || feof($this->stream); 187 | } 188 | 189 | /** 190 | * Returns whether or not the stream is seekable. 191 | */ 192 | public function isSeekable(): bool 193 | { 194 | return $this->seekable; 195 | } 196 | 197 | /** 198 | * {@inheritDoc} 199 | */ 200 | public function seek($offset, $whence = \SEEK_SET): void 201 | { 202 | $this->stream !== null && $this->seekable && fseek($this->stream, $offset, $whence); 203 | } 204 | 205 | /** 206 | * Returns whether or not the stream is writable. 207 | */ 208 | public function isWritable(): bool 209 | { 210 | return $this->writable; 211 | } 212 | 213 | /** 214 | * {@inheritDoc} 215 | */ 216 | public function write($string) 217 | { 218 | $this->size = null; 219 | 220 | return $this->stream !== null && $this->writable ? fwrite($this->stream, $string) : false; 221 | } 222 | 223 | /** 224 | * Returns whether or not the stream is readable. 225 | */ 226 | public function isReadable(): bool 227 | { 228 | return $this->readable; 229 | } 230 | 231 | /** 232 | * {@inheritDoc} 233 | */ 234 | public function read($length): string 235 | { 236 | return $this->stream !== null && $this->readable ? fread($this->stream, $length) : ''; 237 | } 238 | 239 | /** 240 | * Returns the remaining contents in a string. 241 | * 242 | * @throws \RuntimeException if unable to read or an error occurs while 243 | * reading 244 | */ 245 | public function getContents(): string 246 | { 247 | return $this->stream ? stream_get_contents($this->stream) : ''; 248 | } 249 | 250 | /** 251 | * Closes the stream when the destructed. 252 | */ 253 | public function __destruct() 254 | { 255 | $this->close(); 256 | } 257 | 258 | /** 259 | * Closes the stream and any underlying resources. 260 | * 261 | * @psalm-suppress InvalidPropertyAssignmentValue 262 | */ 263 | public function close(): void 264 | { 265 | if (\is_resource($this->stream)) { 266 | fclose($this->stream); 267 | } 268 | $this->detach(); 269 | } 270 | 271 | /** 272 | * Separates any underlying resources from the stream. 273 | * 274 | * After the stream has been detached, the stream is in an unusable state. 275 | * 276 | * @return resource|null Underlying PHP stream, if any 277 | */ 278 | public function detach() 279 | { 280 | $result = $this->stream; 281 | $this->stream = null; 282 | $this->size = null; 283 | $this->uri = null; 284 | $this->readable = false; 285 | $this->writable = false; 286 | $this->seekable = false; 287 | 288 | return $result; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/IO/Stream/ZipEntryStreamWrapper.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\IO\Stream; 13 | 14 | use PhpZip\Exception\ZipException; 15 | use PhpZip\Model\ZipEntry; 16 | 17 | /** 18 | * The class provides stream reuse functionality. 19 | * 20 | * Stream will not be closed at {@see fclose}. 21 | * 22 | * @see https://www.php.net/streamwrapper 23 | */ 24 | final class ZipEntryStreamWrapper 25 | { 26 | /** @var string the registered protocol */ 27 | public const PROTOCOL = 'zipentry'; 28 | 29 | /** @var resource */ 30 | public $context; 31 | 32 | /** @var resource */ 33 | private $fp; 34 | 35 | public static function register(): bool 36 | { 37 | $protocol = self::PROTOCOL; 38 | 39 | if (!\in_array($protocol, stream_get_wrappers(), true)) { 40 | if (!stream_wrapper_register($protocol, self::class)) { 41 | throw new \RuntimeException("Failed to register '{$protocol}://' protocol"); 42 | } 43 | 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | public static function unregister(): void 51 | { 52 | stream_wrapper_unregister(self::PROTOCOL); 53 | } 54 | 55 | /** 56 | * @return resource 57 | */ 58 | public static function wrap(ZipEntry $entry) 59 | { 60 | self::register(); 61 | 62 | $context = stream_context_create( 63 | [ 64 | self::PROTOCOL => [ 65 | 'entry' => $entry, 66 | ], 67 | ] 68 | ); 69 | 70 | $uri = self::PROTOCOL . '://' . $entry->getName(); 71 | $fp = fopen($uri, 'r+b', false, $context); 72 | 73 | if ($fp === false) { 74 | throw new \RuntimeException('Error open ' . $uri); 75 | } 76 | 77 | return $fp; 78 | } 79 | 80 | /** 81 | * Opens file or URL. 82 | * 83 | * This method is called immediately after the wrapper is 84 | * initialized (f.e. by {@see fopen()} and {@see file_get_contents()}). 85 | * 86 | * @param string $path specifies the URL that was passed to 87 | * the original function 88 | * @param string $mode the mode used to open the file, as detailed 89 | * for {@see fopen()} 90 | * @param int $options Holds additional flags set by the streams 91 | * API. It can hold one or more of the 92 | * following values OR'd together. 93 | * @param string|null $opened_path if the path is opened successfully, and 94 | * STREAM_USE_PATH is set in options, 95 | * opened_path should be set to the 96 | * full path of the file/resource that 97 | * was actually opened 98 | * 99 | * @throws ZipException 100 | * 101 | * @see https://www.php.net/streamwrapper.stream-open 102 | */ 103 | public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool 104 | { 105 | if ($this->context === null) { 106 | throw new \RuntimeException('stream context is null'); 107 | } 108 | $streamOptions = stream_context_get_options($this->context); 109 | 110 | if (!isset($streamOptions[self::PROTOCOL]['entry'])) { 111 | throw new \RuntimeException('no stream option ["' . self::PROTOCOL . '"]["entry"]'); 112 | } 113 | $zipEntry = $streamOptions[self::PROTOCOL]['entry']; 114 | 115 | if (!$zipEntry instanceof ZipEntry) { 116 | throw new \RuntimeException('invalid stream context'); 117 | } 118 | 119 | $zipData = $zipEntry->getData(); 120 | 121 | if ($zipData === null) { 122 | throw new ZipException(sprintf('No data for zip entry "%s"', $zipEntry->getName())); 123 | } 124 | $this->fp = $zipData->getDataAsStream(); 125 | 126 | return $this->fp !== false; 127 | } 128 | 129 | /** 130 | * Read from stream. 131 | * 132 | * This method is called in response to {@see fread()} and {@see fgets()}. 133 | * 134 | * Note: Remember to update the read/write position of the stream 135 | * (by the number of bytes that were successfully read). 136 | * 137 | * @param int $count how many bytes of data from the current 138 | * position should be returned 139 | * 140 | * @return false|string If there are less than count bytes available, 141 | * return as many as are available. If no more data 142 | * is available, return either FALSE or 143 | * an empty string. 144 | * 145 | * @see https://www.php.net/streamwrapper.stream-read 146 | */ 147 | public function stream_read(int $count) 148 | { 149 | return fread($this->fp, $count); 150 | } 151 | 152 | /** 153 | * Seeks to specific location in a stream. 154 | * 155 | * This method is called in response to {@see fseek()}. 156 | * The read/write position of the stream should be updated according 157 | * to the offset and whence. 158 | * 159 | * @param int $offset the stream offset to seek to 160 | * @param int $whence Possible values: 161 | * {@see \SEEK_SET} - Set position equal to offset bytes. 162 | * {@see \SEEK_CUR} - Set position to current location plus offset. 163 | * {@see \SEEK_END} - Set position to end-of-file plus offset. 164 | * 165 | * @return bool return TRUE if the position was updated, FALSE otherwise 166 | * 167 | * @see https://www.php.net/streamwrapper.stream-seek 168 | */ 169 | public function stream_seek(int $offset, int $whence = \SEEK_SET): bool 170 | { 171 | return fseek($this->fp, $offset, $whence) === 0; 172 | } 173 | 174 | /** 175 | * Retrieve the current position of a stream. 176 | * 177 | * This method is called in response to {@see fseek()} to determine 178 | * the current position. 179 | * 180 | * @return int should return the current position of the stream 181 | * 182 | * @see https://www.php.net/streamwrapper.stream-tell 183 | */ 184 | public function stream_tell(): int 185 | { 186 | $pos = ftell($this->fp); 187 | 188 | if ($pos === false) { 189 | throw new \RuntimeException('Cannot get stream position.'); 190 | } 191 | 192 | return $pos; 193 | } 194 | 195 | /** 196 | * Tests for end-of-file on a file pointer. 197 | * 198 | * This method is called in response to {@see feof()}. 199 | * 200 | * @return bool should return TRUE if the read/write position is at 201 | * the end of the stream and if no more data is available 202 | * to be read, or FALSE otherwise 203 | * 204 | * @see https://www.php.net/streamwrapper.stream-eof 205 | */ 206 | public function stream_eof(): bool 207 | { 208 | return feof($this->fp); 209 | } 210 | 211 | /** 212 | * Retrieve information about a file resource. 213 | * 214 | * This method is called in response to {@see fstat()}. 215 | * 216 | * @see https://www.php.net/streamwrapper.stream-stat 217 | * @see https://www.php.net/stat 218 | * @see https://www.php.net/fstat 219 | */ 220 | public function stream_stat(): array 221 | { 222 | return fstat($this->fp); 223 | } 224 | 225 | /** 226 | * Flushes the output. 227 | * 228 | * This method is called in response to {@see fflush()} and when the 229 | * stream is being closed while any unflushed data has been written to 230 | * it before. 231 | * If you have cached data in your stream but not yet stored it into 232 | * the underlying storage, you should do so now. 233 | * 234 | * @return bool should return TRUE if the cached data was successfully 235 | * stored (or if there was no data to store), or FALSE 236 | * if the data could not be stored 237 | * 238 | * @see https://www.php.net/streamwrapper.stream-flush 239 | */ 240 | public function stream_flush(): bool 241 | { 242 | return fflush($this->fp); 243 | } 244 | 245 | /** 246 | * Truncate stream. 247 | * 248 | * Will respond to truncation, e.g., through {@see ftruncate()}. 249 | * 250 | * @param int $newSize the new size 251 | * 252 | * @return bool returns TRUE on success or FALSE on failure 253 | * 254 | * @see https://www.php.net/streamwrapper.stream-truncate 255 | */ 256 | public function stream_truncate(int $newSize): bool 257 | { 258 | return ftruncate($this->fp, $newSize); 259 | } 260 | 261 | /** 262 | * Write to stream. 263 | * 264 | * This method is called in response to {@see fwrite().} 265 | * 266 | * Note: Remember to update the current position of the stream by 267 | * number of bytes that were successfully written. 268 | * 269 | * @param string $data should be stored into the underlying stream 270 | * 271 | * @return int should return the number of bytes that were successfully stored, or 0 if none could be stored 272 | * 273 | * @see https://www.php.net/streamwrapper.stream-write 274 | */ 275 | public function stream_write(string $data): int 276 | { 277 | $bytes = fwrite($this->fp, $data); 278 | 279 | return $bytes === false ? 0 : $bytes; 280 | } 281 | 282 | /** 283 | * Retrieve the underlaying resource. 284 | * 285 | * This method is called in response to {@see stream_select()}. 286 | * 287 | * @param int $cast_as can be {@see STREAM_CAST_FOR_SELECT} when {@see stream_select()} 288 | * is callingstream_cast() or {@see STREAM_CAST_AS_STREAM} when 289 | * stream_cast() is called for other uses 290 | * 291 | * @return resource 292 | */ 293 | public function stream_cast(int $cast_as) 294 | { 295 | return $this->fp; 296 | } 297 | 298 | /** 299 | * Close a resource. 300 | * 301 | * This method is called in response to {@see fclose()}. 302 | * All resources that were locked, or allocated, by the wrapper should be released. 303 | * 304 | * @see https://www.php.net/streamwrapper.stream-close 305 | */ 306 | public function stream_close(): void 307 | { 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/Model/Data/ZipFileData.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Data; 13 | 14 | use PhpZip\Exception\ZipException; 15 | use PhpZip\Model\ZipData; 16 | use PhpZip\Model\ZipEntry; 17 | 18 | class ZipFileData implements ZipData 19 | { 20 | private \SplFileInfo $file; 21 | 22 | /** 23 | * @throws ZipException 24 | */ 25 | public function __construct(ZipEntry $zipEntry, \SplFileInfo $fileInfo) 26 | { 27 | if (!$fileInfo->isFile()) { 28 | throw new ZipException('$fileInfo is not a file.'); 29 | } 30 | 31 | if (!$fileInfo->isReadable()) { 32 | throw new ZipException('$fileInfo is not readable.'); 33 | } 34 | 35 | $this->file = $fileInfo; 36 | $zipEntry->setUncompressedSize($fileInfo->getSize()); 37 | } 38 | 39 | /** 40 | * @throws ZipException 41 | * 42 | * @return resource returns stream data 43 | */ 44 | public function getDataAsStream() 45 | { 46 | if (!$this->file->isReadable()) { 47 | throw new ZipException(sprintf('The %s file is no longer readable.', $this->file->getPathname())); 48 | } 49 | 50 | return fopen($this->file->getPathname(), 'rb'); 51 | } 52 | 53 | /** 54 | * @throws ZipException 55 | * 56 | * @return string returns data as string 57 | */ 58 | public function getDataAsString(): string 59 | { 60 | if (!$this->file->isReadable()) { 61 | throw new ZipException(sprintf('The %s file is no longer readable.', $this->file->getPathname())); 62 | } 63 | 64 | return file_get_contents($this->file->getPathname()); 65 | } 66 | 67 | /** 68 | * @param resource $outStream 69 | * 70 | * @throws ZipException 71 | */ 72 | public function copyDataToStream($outStream): void 73 | { 74 | $stream = $this->getDataAsStream(); 75 | stream_copy_to_stream($stream, $outStream); 76 | fclose($stream); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Model/Data/ZipNewData.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Data; 13 | 14 | use PhpZip\Model\ZipData; 15 | use PhpZip\Model\ZipEntry; 16 | use PhpZip\ZipFile; 17 | 18 | /** 19 | * The class contains a streaming resource with new content added to the ZIP archive. 20 | */ 21 | class ZipNewData implements ZipData 22 | { 23 | /** 24 | * A static variable allows closing the stream in the destructor 25 | * only if it is its sole holder. 26 | * 27 | * @var array array of resource ids and the number of class clones 28 | */ 29 | private static array $guardClonedStream = []; 30 | 31 | private ZipEntry $zipEntry; 32 | 33 | /** @var resource */ 34 | private $stream; 35 | 36 | /** 37 | * @param string|resource $data Raw string data or resource 38 | * @noinspection PhpMissingParamTypeInspection 39 | */ 40 | public function __construct(ZipEntry $zipEntry, $data) 41 | { 42 | $this->zipEntry = $zipEntry; 43 | 44 | if (\is_string($data)) { 45 | $zipEntry->setUncompressedSize(\strlen($data)); 46 | 47 | if (!($handle = fopen('php://temp', 'w+b'))) { 48 | // @codeCoverageIgnoreStart 49 | throw new \RuntimeException('A temporary resource cannot be opened for writing.'); 50 | // @codeCoverageIgnoreEnd 51 | } 52 | fwrite($handle, $data); 53 | rewind($handle); 54 | $this->stream = $handle; 55 | } elseif (\is_resource($data)) { 56 | $this->stream = $data; 57 | } 58 | 59 | $resourceId = (int) $this->stream; 60 | self::$guardClonedStream[$resourceId] 61 | = isset(self::$guardClonedStream[$resourceId]) 62 | ? self::$guardClonedStream[$resourceId] + 1 63 | : 0; 64 | } 65 | 66 | /** 67 | * @return resource returns stream data 68 | */ 69 | public function getDataAsStream() 70 | { 71 | if (!\is_resource($this->stream)) { 72 | throw new \LogicException(sprintf('Resource has been closed (entry=%s).', $this->zipEntry->getName())); 73 | } 74 | 75 | return $this->stream; 76 | } 77 | 78 | /** 79 | * @return string returns data as string 80 | */ 81 | public function getDataAsString(): string 82 | { 83 | $stream = $this->getDataAsStream(); 84 | $pos = ftell($stream); 85 | 86 | try { 87 | rewind($stream); 88 | 89 | return stream_get_contents($stream); 90 | } finally { 91 | fseek($stream, $pos); 92 | } 93 | } 94 | 95 | /** 96 | * @param resource $outStream 97 | */ 98 | public function copyDataToStream($outStream): void 99 | { 100 | $stream = $this->getDataAsStream(); 101 | rewind($stream); 102 | stream_copy_to_stream($stream, $outStream); 103 | } 104 | 105 | /** 106 | * @see https://php.net/manual/en/language.oop5.cloning.php 107 | */ 108 | public function __clone() 109 | { 110 | $resourceId = (int) $this->stream; 111 | self::$guardClonedStream[$resourceId] 112 | = isset(self::$guardClonedStream[$resourceId]) 113 | ? self::$guardClonedStream[$resourceId] + 1 114 | : 1; 115 | } 116 | 117 | /** 118 | * The stream will be closed when closing the zip archive. 119 | * 120 | * The method implements protection against closing the stream of the cloned object. 121 | * 122 | * @see ZipFile::close() 123 | */ 124 | public function __destruct() 125 | { 126 | $resourceId = (int) $this->stream; 127 | 128 | if (isset(self::$guardClonedStream[$resourceId]) && self::$guardClonedStream[$resourceId] > 0) { 129 | self::$guardClonedStream[$resourceId]--; 130 | 131 | return; 132 | } 133 | 134 | if (\is_resource($this->stream)) { 135 | fclose($this->stream); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Model/Data/ZipSourceFileData.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Data; 13 | 14 | use PhpZip\Exception\Crc32Exception; 15 | use PhpZip\Exception\ZipException; 16 | use PhpZip\IO\ZipReader; 17 | use PhpZip\Model\ZipData; 18 | use PhpZip\Model\ZipEntry; 19 | 20 | class ZipSourceFileData implements ZipData 21 | { 22 | private ZipReader $zipReader; 23 | 24 | /** @var resource|null */ 25 | private $stream; 26 | 27 | private ZipEntry $sourceEntry; 28 | 29 | private int $offset; 30 | 31 | private int $uncompressedSize; 32 | 33 | private int $compressedSize; 34 | 35 | public function __construct(ZipReader $zipReader, ZipEntry $zipEntry, int $offsetData) 36 | { 37 | $this->zipReader = $zipReader; 38 | $this->offset = $offsetData; 39 | $this->sourceEntry = $zipEntry; 40 | $this->compressedSize = $zipEntry->getCompressedSize(); 41 | $this->uncompressedSize = $zipEntry->getUncompressedSize(); 42 | } 43 | 44 | public function hasRecompressData(ZipEntry $entry): bool 45 | { 46 | return $this->sourceEntry->getCompressionLevel() !== $entry->getCompressionLevel() 47 | || $this->sourceEntry->getCompressionMethod() !== $entry->getCompressionMethod() 48 | || $this->sourceEntry->isEncrypted() !== $entry->isEncrypted() 49 | || $this->sourceEntry->getEncryptionMethod() !== $entry->getEncryptionMethod() 50 | || $this->sourceEntry->getPassword() !== $entry->getPassword() 51 | || $this->sourceEntry->getCompressedSize() !== $entry->getCompressedSize() 52 | || $this->sourceEntry->getUncompressedSize() !== $entry->getUncompressedSize() 53 | || $this->sourceEntry->getCrc() !== $entry->getCrc(); 54 | } 55 | 56 | /** 57 | * @throws ZipException 58 | * 59 | * @return resource returns stream data 60 | */ 61 | public function getDataAsStream() 62 | { 63 | if (!\is_resource($this->stream)) { 64 | $this->stream = $this->zipReader->getEntryStream($this); 65 | } 66 | 67 | return $this->stream; 68 | } 69 | 70 | /** 71 | * @throws ZipException 72 | * 73 | * @return string returns data as string 74 | */ 75 | public function getDataAsString(): string 76 | { 77 | $autoClosable = $this->stream === null; 78 | 79 | $stream = $this->getDataAsStream(); 80 | $pos = ftell($stream); 81 | 82 | try { 83 | rewind($stream); 84 | 85 | return stream_get_contents($stream); 86 | } finally { 87 | if ($autoClosable) { 88 | fclose($stream); 89 | $this->stream = null; 90 | } else { 91 | fseek($stream, $pos); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * @param resource $outStream Output stream 98 | * 99 | * @throws ZipException 100 | * @throws Crc32Exception 101 | */ 102 | public function copyDataToStream($outStream): void 103 | { 104 | if (\is_resource($this->stream)) { 105 | rewind($this->stream); 106 | stream_copy_to_stream($this->stream, $outStream); 107 | } else { 108 | $this->zipReader->copyUncompressedDataToStream($this, $outStream); 109 | } 110 | } 111 | 112 | /** 113 | * @param resource $outputStream Output stream 114 | */ 115 | public function copyCompressedDataToStream($outputStream): void 116 | { 117 | $this->zipReader->copyCompressedDataToStream($this, $outputStream); 118 | } 119 | 120 | public function getSourceEntry(): ZipEntry 121 | { 122 | return $this->sourceEntry; 123 | } 124 | 125 | public function getCompressedSize(): int 126 | { 127 | return $this->compressedSize; 128 | } 129 | 130 | public function getUncompressedSize(): int 131 | { 132 | return $this->uncompressedSize; 133 | } 134 | 135 | public function getOffset(): int 136 | { 137 | return $this->offset; 138 | } 139 | 140 | public function __destruct() 141 | { 142 | if (\is_resource($this->stream)) { 143 | fclose($this->stream); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Model/EndOfCentralDirectory.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model; 13 | 14 | /** 15 | * End of Central Directory. 16 | */ 17 | class EndOfCentralDirectory 18 | { 19 | /** @var int Count files. */ 20 | private int $entryCount; 21 | 22 | /** @var int Central Directory Offset. */ 23 | private int $cdOffset; 24 | 25 | private int $cdSize; 26 | 27 | /** @var string|null The archive comment. */ 28 | private ?string $comment; 29 | 30 | /** @var bool Zip64 extension */ 31 | private bool $zip64; 32 | 33 | public function __construct(int $entryCount, int $cdOffset, int $cdSize, bool $zip64, ?string $comment = null) 34 | { 35 | $this->entryCount = $entryCount; 36 | $this->cdOffset = $cdOffset; 37 | $this->cdSize = $cdSize; 38 | $this->zip64 = $zip64; 39 | $this->comment = $comment; 40 | } 41 | 42 | public function setComment(?string $comment): void 43 | { 44 | $this->comment = $comment; 45 | } 46 | 47 | public function getEntryCount(): int 48 | { 49 | return $this->entryCount; 50 | } 51 | 52 | public function getCdOffset(): int 53 | { 54 | return $this->cdOffset; 55 | } 56 | 57 | public function getCdSize(): int 58 | { 59 | return $this->cdSize; 60 | } 61 | 62 | public function getComment(): ?string 63 | { 64 | return $this->comment; 65 | } 66 | 67 | public function isZip64(): bool 68 | { 69 | return $this->zip64; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Model/Extra/ExtraFieldsCollection.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra; 13 | 14 | /** 15 | * Represents a collection of Extra Fields as they may 16 | * be present at several locations in ZIP files. 17 | */ 18 | class ExtraFieldsCollection implements \ArrayAccess, \Countable, \Iterator 19 | { 20 | /** 21 | * The map of Extra Fields. 22 | * Maps from Header ID to Extra Field. 23 | * Must not be null, but may be empty if no Extra Fields are used. 24 | * The map is sorted by Header IDs in ascending order. 25 | * 26 | * @var ZipExtraField[] 27 | */ 28 | protected array $collection = []; 29 | 30 | /** 31 | * Returns the number of Extra Fields in this collection. 32 | */ 33 | public function count(): int 34 | { 35 | return \count($this->collection); 36 | } 37 | 38 | /** 39 | * Returns the Extra Field with the given Header ID or null 40 | * if no such Extra Field exists. 41 | * 42 | * @param int $headerId the requested Header ID 43 | * 44 | * @return ZipExtraField|null the Extra Field with the given Header ID or 45 | * if no such Extra Field exists 46 | */ 47 | public function get(int $headerId): ?ZipExtraField 48 | { 49 | $this->validateHeaderId($headerId); 50 | 51 | return $this->collection[$headerId] ?? null; 52 | } 53 | 54 | private function validateHeaderId(int $headerId): void 55 | { 56 | if ($headerId < 0 || $headerId > 0xFFFF) { 57 | throw new \InvalidArgumentException('$headerId out of range'); 58 | } 59 | } 60 | 61 | /** 62 | * Stores the given Extra Field in this collection. 63 | * 64 | * @param ZipExtraField $extraField the Extra Field to store in this collection 65 | * 66 | * @return ZipExtraField the Extra Field previously associated with the Header ID of 67 | * of the given Extra Field or null if no such Extra Field existed 68 | */ 69 | public function add(ZipExtraField $extraField): ZipExtraField 70 | { 71 | $headerId = $extraField->getHeaderId(); 72 | 73 | $this->validateHeaderId($headerId); 74 | $this->collection[$headerId] = $extraField; 75 | 76 | return $extraField; 77 | } 78 | 79 | /** 80 | * @param ZipExtraField[] $extraFields 81 | */ 82 | public function addAll(array $extraFields): void 83 | { 84 | foreach ($extraFields as $extraField) { 85 | $this->add($extraField); 86 | } 87 | } 88 | 89 | /** 90 | * @param ExtraFieldsCollection $collection 91 | */ 92 | public function addCollection(self $collection): void 93 | { 94 | $this->addAll($collection->collection); 95 | } 96 | 97 | /** 98 | * @return ZipExtraField[] 99 | */ 100 | public function getAll(): array 101 | { 102 | return $this->collection; 103 | } 104 | 105 | /** 106 | * Returns Extra Field exists. 107 | * 108 | * @param int $headerId the requested Header ID 109 | */ 110 | public function has(int $headerId): bool 111 | { 112 | return isset($this->collection[$headerId]); 113 | } 114 | 115 | /** 116 | * Removes the Extra Field with the given Header ID. 117 | * 118 | * @param int $headerId the requested Header ID 119 | * 120 | * @return ZipExtraField|null the Extra Field with the given Header ID or null 121 | * if no such Extra Field exists 122 | */ 123 | public function remove(int $headerId): ?ZipExtraField 124 | { 125 | $this->validateHeaderId($headerId); 126 | 127 | if (isset($this->collection[$headerId])) { 128 | $ef = $this->collection[$headerId]; 129 | unset($this->collection[$headerId]); 130 | 131 | return $ef; 132 | } 133 | 134 | return null; 135 | } 136 | 137 | /** 138 | * Whether a offset exists. 139 | * 140 | * @see http://php.net/manual/en/arrayaccess.offsetexists.php 141 | * 142 | * @param mixed $offset an offset to check for 143 | * 144 | * @return bool true on success or false on failure 145 | */ 146 | public function offsetExists($offset): bool 147 | { 148 | return isset($this->collection[(int) $offset]); 149 | } 150 | 151 | /** 152 | * Offset to retrieve. 153 | * 154 | * @see http://php.net/manual/en/arrayaccess.offsetget.php 155 | * 156 | * @param mixed $offset the offset to retrieve 157 | */ 158 | public function offsetGet($offset): ?ZipExtraField 159 | { 160 | return $this->collection[(int) $offset] ?? null; 161 | } 162 | 163 | /** 164 | * Offset to set. 165 | * 166 | * @see http://php.net/manual/en/arrayaccess.offsetset.php 167 | * 168 | * @param mixed $offset the offset to assign the value to 169 | * @param mixed $value the value to set 170 | */ 171 | public function offsetSet($offset, $value): void 172 | { 173 | if (!$value instanceof ZipExtraField) { 174 | throw new \InvalidArgumentException('value is not instanceof ' . ZipExtraField::class); 175 | } 176 | $this->add($value); 177 | } 178 | 179 | /** 180 | * Offset to unset. 181 | * 182 | * @see http://php.net/manual/en/arrayaccess.offsetunset.php 183 | * 184 | * @param mixed $offset the offset to unset 185 | */ 186 | public function offsetUnset($offset): void 187 | { 188 | $this->remove($offset); 189 | } 190 | 191 | /** 192 | * Return the current element. 193 | * 194 | * @see http://php.net/manual/en/iterator.current.php 195 | */ 196 | public function current(): ZipExtraField 197 | { 198 | return current($this->collection); 199 | } 200 | 201 | /** 202 | * Move forward to next element. 203 | * 204 | * @see http://php.net/manual/en/iterator.next.php 205 | */ 206 | public function next(): void 207 | { 208 | next($this->collection); 209 | } 210 | 211 | /** 212 | * Return the key of the current element. 213 | * 214 | * @see http://php.net/manual/en/iterator.key.php 215 | * 216 | * @return int scalar on success, or null on failure 217 | */ 218 | public function key(): int 219 | { 220 | return key($this->collection); 221 | } 222 | 223 | /** 224 | * Checks if current position is valid. 225 | * 226 | * @see http://php.net/manual/en/iterator.valid.php 227 | * 228 | * @return bool The return value will be casted to boolean and then evaluated. 229 | * Returns true on success or false on failure. 230 | */ 231 | public function valid(): bool 232 | { 233 | return key($this->collection) !== null; 234 | } 235 | 236 | /** 237 | * Rewind the Iterator to the first element. 238 | * 239 | * @see http://php.net/manual/en/iterator.rewind.php 240 | */ 241 | public function rewind(): void 242 | { 243 | reset($this->collection); 244 | } 245 | 246 | public function clear(): void 247 | { 248 | $this->collection = []; 249 | } 250 | 251 | public function __toString(): string 252 | { 253 | $formats = []; 254 | 255 | foreach ($this->collection as $key => $value) { 256 | $formats[] = (string) $value; 257 | } 258 | 259 | return implode("\n", $formats); 260 | } 261 | 262 | /** 263 | * If clone extra fields. 264 | */ 265 | public function __clone() 266 | { 267 | foreach ($this->collection as $k => $v) { 268 | $this->collection[$k] = clone $v; 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/AbstractUnicodeExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Exception\ZipException; 15 | use PhpZip\Model\Extra\ZipExtraField; 16 | use PhpZip\Model\ZipEntry; 17 | 18 | /** 19 | * A common base class for Unicode extra information extra fields. 20 | */ 21 | abstract class AbstractUnicodeExtraField implements ZipExtraField 22 | { 23 | public const DEFAULT_VERSION = 0x01; 24 | 25 | private int $crc32; 26 | 27 | private string $unicodeValue; 28 | 29 | public function __construct(int $crc32, string $unicodeValue) 30 | { 31 | $this->crc32 = $crc32; 32 | $this->unicodeValue = $unicodeValue; 33 | } 34 | 35 | /** 36 | * @return int the CRC32 checksum of the filename or comment as 37 | * encoded in the central directory of the zip file 38 | */ 39 | public function getCrc32(): int 40 | { 41 | return $this->crc32; 42 | } 43 | 44 | public function setCrc32(int $crc32): void 45 | { 46 | $this->crc32 = $crc32; 47 | } 48 | 49 | public function getUnicodeValue(): string 50 | { 51 | return $this->unicodeValue; 52 | } 53 | 54 | /** 55 | * @param string $unicodeValue the UTF-8 encoded name to set 56 | */ 57 | public function setUnicodeValue(string $unicodeValue): void 58 | { 59 | $this->unicodeValue = $unicodeValue; 60 | } 61 | 62 | /** 63 | * Populate data from this array as if it was in local file data. 64 | * 65 | * @param string $buffer the buffer to read data from 66 | * @param ZipEntry|null $entry optional zip entry 67 | * 68 | * @throws ZipException on error 69 | * 70 | * @return static 71 | */ 72 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 73 | { 74 | if (\strlen($buffer) < 5) { 75 | throw new ZipException('Unicode path extra data must have at least 5 bytes.'); 76 | } 77 | 78 | [ 79 | 'version' => $version, 80 | 'crc32' => $crc32, 81 | ] = unpack('Cversion/Vcrc32', $buffer); 82 | 83 | if ($version !== self::DEFAULT_VERSION) { 84 | throw new ZipException(sprintf('Unsupported version [%d] for Unicode path extra data.', $version)); 85 | } 86 | 87 | $unicodeValue = substr($buffer, 5); 88 | 89 | return new static($crc32, $unicodeValue); 90 | } 91 | 92 | /** 93 | * Populate data from this array as if it was in central directory data. 94 | * 95 | * @param string $buffer the buffer to read data from 96 | * @param ZipEntry|null $entry optional zip entry 97 | * 98 | * @throws ZipException on error 99 | * 100 | * @return static 101 | */ 102 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 103 | { 104 | return self::unpackLocalFileData($buffer, $entry); 105 | } 106 | 107 | /** 108 | * The actual data to put into local file data - without Header-ID 109 | * or length specifier. 110 | * 111 | * @return string the data 112 | */ 113 | public function packLocalFileData(): string 114 | { 115 | return pack( 116 | 'CV', 117 | self::DEFAULT_VERSION, 118 | $this->crc32 119 | ) 120 | . $this->unicodeValue; 121 | } 122 | 123 | /** 124 | * The actual data to put into central directory - without Header-ID or 125 | * length specifier. 126 | * 127 | * @return string the data 128 | */ 129 | public function packCentralDirData(): string 130 | { 131 | return $this->packLocalFileData(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/ApkAlignmentExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Exception\ZipException; 15 | use PhpZip\Model\Extra\ZipExtraField; 16 | use PhpZip\Model\ZipEntry; 17 | 18 | /** 19 | * Apk Alignment Extra Field. 20 | * 21 | * @see https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/ApkSigner.java 22 | * @see https://developer.android.com/studio/command-line/zipalign 23 | */ 24 | final class ApkAlignmentExtraField implements ZipExtraField 25 | { 26 | /** 27 | * @var int Extensible data block/field header ID used for storing 28 | * information about alignment of uncompressed entries as 29 | * well as for aligning the entries's data. See ZIP 30 | * appnote.txt section 4.5 Extensible data fields. 31 | */ 32 | public const HEADER_ID = 0xD935; 33 | 34 | /** @var int */ 35 | public const ALIGNMENT_BYTES = 4; 36 | 37 | /** @var int */ 38 | public const COMMON_PAGE_ALIGNMENT_BYTES = 4096; 39 | 40 | private int $multiple; 41 | 42 | private int $padding; 43 | 44 | public function __construct(int $multiple, int $padding) 45 | { 46 | $this->multiple = $multiple; 47 | $this->padding = $padding; 48 | } 49 | 50 | /** 51 | * Returns the Header ID (type) of this Extra Field. 52 | * The Header ID is an unsigned short integer (two bytes) 53 | * which must be constant during the life cycle of this object. 54 | */ 55 | public function getHeaderId(): int 56 | { 57 | return self::HEADER_ID; 58 | } 59 | 60 | public function getMultiple(): int 61 | { 62 | return $this->multiple; 63 | } 64 | 65 | public function getPadding(): int 66 | { 67 | return $this->padding; 68 | } 69 | 70 | public function setMultiple(int $multiple): void 71 | { 72 | $this->multiple = $multiple; 73 | } 74 | 75 | public function setPadding(int $padding): void 76 | { 77 | $this->padding = $padding; 78 | } 79 | 80 | /** 81 | * Populate data from this array as if it was in local file data. 82 | * 83 | * @param string $buffer the buffer to read data from 84 | * @param ZipEntry|null $entry optional zip entry 85 | * 86 | * @throws ZipException 87 | * 88 | * @return ApkAlignmentExtraField 89 | */ 90 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 91 | { 92 | $length = \strlen($buffer); 93 | 94 | if ($length < 2) { 95 | // This is APK alignment field. 96 | // FORMAT: 97 | // * uint16 alignment multiple (in bytes) 98 | // * remaining bytes -- padding to achieve alignment of data which starts after 99 | // the extra field 100 | throw new ZipException( 101 | 'Minimum 6 bytes of the extensible data block/field used for alignment of uncompressed entries.' 102 | ); 103 | } 104 | $multiple = unpack('v', $buffer)[1]; 105 | $padding = $length - 2; 106 | 107 | return new self($multiple, $padding); 108 | } 109 | 110 | /** 111 | * Populate data from this array as if it was in central directory data. 112 | * 113 | * @param string $buffer the buffer to read data from 114 | * @param ZipEntry|null $entry optional zip entry 115 | * 116 | * @throws ZipException on error 117 | * 118 | * @return ApkAlignmentExtraField 119 | */ 120 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 121 | { 122 | return self::unpackLocalFileData($buffer, $entry); 123 | } 124 | 125 | /** 126 | * The actual data to put into local file data - without Header-ID 127 | * or length specifier. 128 | * 129 | * @return string the data 130 | */ 131 | public function packLocalFileData(): string 132 | { 133 | return pack('vx' . $this->padding, $this->multiple); 134 | } 135 | 136 | /** 137 | * The actual data to put into central directory - without Header-ID or 138 | * length specifier. 139 | * 140 | * @return string the data 141 | */ 142 | public function packCentralDirData(): string 143 | { 144 | return $this->packLocalFileData(); 145 | } 146 | 147 | public function __toString(): string 148 | { 149 | return sprintf( 150 | '0x%04x APK Alignment: Multiple=%d Padding=%d', 151 | self::HEADER_ID, 152 | $this->multiple, 153 | $this->padding 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/AsiExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Constants\UnixStat; 15 | use PhpZip\Exception\Crc32Exception; 16 | use PhpZip\Model\Extra\ZipExtraField; 17 | use PhpZip\Model\ZipEntry; 18 | 19 | /** 20 | * ASi Unix Extra Field: 21 | * ====================. 22 | * 23 | * The following is the layout of the ASi extra block for Unix. The 24 | * local-header and central-header versions are identical. 25 | * (Last Revision 19960916) 26 | * 27 | * Value Size Description 28 | * ----- ---- ----------- 29 | * (Unix3) 0x756e Short tag for this extra block type ("nu") 30 | * TSize Short total data size for this block 31 | * CRC Long CRC-32 of the remaining data 32 | * Mode Short file permissions 33 | * SizDev Long symlink'd size OR major/minor dev num 34 | * UID Short user ID 35 | * GID Short group ID 36 | * (var.) variable symbolic link filename 37 | * 38 | * Mode is the standard Unix st_mode field from struct stat, containing 39 | * user/group/other permissions, setuid/setgid and symlink info, etc. 40 | * 41 | * If Mode indicates that this file is a symbolic link, SizDev is the 42 | * size of the file to which the link points. Otherwise, if the file 43 | * is a device, SizDev contains the standard Unix st_rdev field from 44 | * struct stat (includes the major and minor numbers of the device). 45 | * SizDev is undefined in other cases. 46 | * 47 | * If Mode indicates that the file is a symbolic link, the final field 48 | * will be the name of the file to which the link points. The file- 49 | * name length can be inferred from TSize. 50 | * 51 | * [Note that TSize may incorrectly refer to the data size not counting 52 | * the CRC; i.e., it may be four bytes too small.] 53 | * 54 | * @see ftp://ftp.info-zip.org/pub/infozip/doc/appnote-iz-latest.zip Info-ZIP version Specification 55 | */ 56 | final class AsiExtraField implements ZipExtraField 57 | { 58 | /** @var int Header id */ 59 | public const HEADER_ID = 0x756E; 60 | 61 | public const USER_GID_PID = 1000; 62 | 63 | /** Bits used for permissions (and sticky bit). */ 64 | public const PERM_MASK = 07777; 65 | 66 | /** @var int Standard Unix stat(2) file mode. */ 67 | private int $mode; 68 | 69 | /** @var int User ID. */ 70 | private int $uid; 71 | 72 | /** @var int Group ID. */ 73 | private int $gid; 74 | 75 | /** 76 | * @var string File this entry points to, if it is a symbolic link. 77 | * Empty string - if entry is not a symbolic link. 78 | */ 79 | private string $link; 80 | 81 | public function __construct(int $mode, int $uid = self::USER_GID_PID, int $gid = self::USER_GID_PID, string $link = '') 82 | { 83 | $this->mode = $mode; 84 | $this->uid = $uid; 85 | $this->gid = $gid; 86 | $this->link = $link; 87 | } 88 | 89 | /** 90 | * Returns the Header ID (type) of this Extra Field. 91 | * The Header ID is an unsigned short integer (two bytes) 92 | * which must be constant during the life cycle of this object. 93 | */ 94 | public function getHeaderId(): int 95 | { 96 | return self::HEADER_ID; 97 | } 98 | 99 | /** 100 | * Populate data from this array as if it was in local file data. 101 | * 102 | * @param string $buffer the buffer to read data from 103 | * @param ZipEntry|null $entry optional zip entry 104 | * 105 | * @throws Crc32Exception 106 | * 107 | * @return AsiExtraField 108 | */ 109 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 110 | { 111 | $givenChecksum = unpack('V', $buffer)[1]; 112 | $buffer = substr($buffer, 4); 113 | $realChecksum = crc32($buffer); 114 | 115 | if ($givenChecksum !== $realChecksum) { 116 | throw new Crc32Exception('Asi Unix Extra Filed Data', $givenChecksum, $realChecksum); 117 | } 118 | 119 | [ 120 | 'mode' => $mode, 121 | 'linkSize' => $linkSize, 122 | 'uid' => $uid, 123 | 'gid' => $gid, 124 | ] = unpack('vmode/VlinkSize/vuid/vgid', $buffer); 125 | $link = ''; 126 | 127 | if ($linkSize > 0) { 128 | $link = substr($buffer, 10); 129 | } 130 | 131 | return new self($mode, $uid, $gid, $link); 132 | } 133 | 134 | /** 135 | * Populate data from this array as if it was in central directory data. 136 | * 137 | * @param string $buffer the buffer to read data from 138 | * @param ZipEntry|null $entry optional zip entry 139 | * 140 | * @throws Crc32Exception 141 | * 142 | * @return AsiExtraField 143 | */ 144 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 145 | { 146 | return self::unpackLocalFileData($buffer, $entry); 147 | } 148 | 149 | /** 150 | * The actual data to put into local file data - without Header-ID 151 | * or length specifier. 152 | * 153 | * @return string the data 154 | */ 155 | public function packLocalFileData(): string 156 | { 157 | $data = pack( 158 | 'vVvv', 159 | $this->mode, 160 | \strlen($this->link), 161 | $this->uid, 162 | $this->gid 163 | ) . $this->link; 164 | 165 | return pack('V', crc32($data)) . $data; 166 | } 167 | 168 | /** 169 | * The actual data to put into central directory - without Header-ID or 170 | * length specifier. 171 | * 172 | * @return string the data 173 | */ 174 | public function packCentralDirData(): string 175 | { 176 | return $this->packLocalFileData(); 177 | } 178 | 179 | /** 180 | * Name of linked file. 181 | * 182 | * @return string name of the file this entry links to if it is a 183 | * symbolic link, the empty string otherwise 184 | */ 185 | public function getLink(): string 186 | { 187 | return $this->link; 188 | } 189 | 190 | /** 191 | * Indicate that this entry is a symbolic link to the given filename. 192 | * 193 | * @param string $link name of the file this entry links to, empty 194 | * string if it is not a symbolic link 195 | */ 196 | public function setLink(string $link): void 197 | { 198 | $this->link = $link; 199 | $this->mode = $this->getPermissionsMode($this->mode); 200 | } 201 | 202 | /** 203 | * Is this entry a symbolic link? 204 | * 205 | * @return bool true if this is a symbolic link 206 | */ 207 | public function isLink(): bool 208 | { 209 | return !empty($this->link); 210 | } 211 | 212 | /** 213 | * Get the file mode for given permissions with the correct file type. 214 | * 215 | * @param int $mode the mode 216 | * 217 | * @return int the type with the mode 218 | */ 219 | private function getPermissionsMode(int $mode): int 220 | { 221 | $type = 0; 222 | 223 | if ($this->isLink()) { 224 | $type = UnixStat::UNX_IFLNK; 225 | } elseif (($mode & UnixStat::UNX_IFREG) !== 0) { 226 | $type = UnixStat::UNX_IFREG; 227 | } elseif (($mode & UnixStat::UNX_IFDIR) !== 0) { 228 | $type = UnixStat::UNX_IFDIR; 229 | } 230 | 231 | return $type | ($mode & self::PERM_MASK); 232 | } 233 | 234 | /** 235 | * Is this entry a directory? 236 | * 237 | * @return bool true if this entry is a directory 238 | */ 239 | public function isDirectory(): bool 240 | { 241 | return ($this->mode & UnixStat::UNX_IFDIR) !== 0 && !$this->isLink(); 242 | } 243 | 244 | public function getMode(): int 245 | { 246 | return $this->mode; 247 | } 248 | 249 | public function setMode(int $mode): void 250 | { 251 | $this->mode = $this->getPermissionsMode($mode); 252 | } 253 | 254 | public function getUserId(): int 255 | { 256 | return $this->uid; 257 | } 258 | 259 | public function setUserId(int $uid): void 260 | { 261 | $this->uid = $uid; 262 | } 263 | 264 | public function getGroupId(): int 265 | { 266 | return $this->gid; 267 | } 268 | 269 | public function setGroupId(int $gid): void 270 | { 271 | $this->gid = $gid; 272 | } 273 | 274 | public function __toString(): string 275 | { 276 | return sprintf( 277 | '0x%04x ASI: Mode=%o UID=%d GID=%d Link="%s', 278 | self::HEADER_ID, 279 | $this->mode, 280 | $this->uid, 281 | $this->gid, 282 | $this->link 283 | ); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/JarMarkerExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Exception\ZipException; 15 | use PhpZip\Model\Extra\ZipExtraField; 16 | use PhpZip\Model\ZipContainer; 17 | use PhpZip\Model\ZipEntry; 18 | 19 | /** 20 | * Jar Marker Extra Field. 21 | * An executable Java program can be packaged in a JAR file with all the libraries it uses. 22 | * Executable JAR files can easily be distinguished from the files packed in the JAR file 23 | * by the extra field in the first file, which is hexadecimal in the 0xCAFE bytes series. 24 | * If this extra field is added as the very first extra field of 25 | * the archive, Solaris will consider it an executable jar file. 26 | */ 27 | final class JarMarkerExtraField implements ZipExtraField 28 | { 29 | /** @var int Header id. */ 30 | public const HEADER_ID = 0xCAFE; 31 | 32 | public static function setJarMarker(ZipContainer $container): void 33 | { 34 | $zipEntries = $container->getEntries(); 35 | 36 | if (!empty($zipEntries)) { 37 | foreach ($zipEntries as $zipEntry) { 38 | $zipEntry->removeExtraField(self::HEADER_ID); 39 | } 40 | // set jar execute bit 41 | reset($zipEntries); 42 | $zipEntry = current($zipEntries); 43 | $zipEntry->getCdExtraFields()[] = new self(); 44 | } 45 | } 46 | 47 | /** 48 | * Returns the Header ID (type) of this Extra Field. 49 | * The Header ID is an unsigned short integer (two bytes) 50 | * which must be constant during the life cycle of this object. 51 | */ 52 | public function getHeaderId(): int 53 | { 54 | return self::HEADER_ID; 55 | } 56 | 57 | /** 58 | * The actual data to put into local file data - without Header-ID 59 | * or length specifier. 60 | * 61 | * @return string the data 62 | */ 63 | public function packLocalFileData(): string 64 | { 65 | return ''; 66 | } 67 | 68 | /** 69 | * The actual data to put into central directory - without Header-ID or 70 | * length specifier. 71 | * 72 | * @return string the data 73 | */ 74 | public function packCentralDirData(): string 75 | { 76 | return ''; 77 | } 78 | 79 | /** 80 | * Populate data from this array as if it was in local file data. 81 | * 82 | * @param string $buffer the buffer to read data from 83 | * @param ZipEntry|null $entry optional zip entry 84 | * 85 | * @throws ZipException on error 86 | * 87 | * @return JarMarkerExtraField 88 | */ 89 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 90 | { 91 | if (!empty($buffer)) { 92 | throw new ZipException("JarMarker doesn't expect any data"); 93 | } 94 | 95 | return new self(); 96 | } 97 | 98 | /** 99 | * Populate data from this array as if it was in central directory data. 100 | * 101 | * @param string $buffer the buffer to read data from 102 | * @param ZipEntry|null $entry optional zip entry 103 | * 104 | * @throws ZipException on error 105 | * 106 | * @return JarMarkerExtraField 107 | */ 108 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 109 | { 110 | return self::unpackLocalFileData($buffer, $entry); 111 | } 112 | 113 | public function __toString(): string 114 | { 115 | return sprintf('0x%04x Jar Marker', self::HEADER_ID); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/NewUnixExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Exception\ZipException; 15 | use PhpZip\Model\Extra\ZipExtraField; 16 | use PhpZip\Model\ZipEntry; 17 | 18 | /** 19 | * Info-ZIP New Unix Extra Field: 20 | * ====================================. 21 | * 22 | * Currently stores Unix UIDs/GIDs up to 32 bits. 23 | * (Last Revision 20080509) 24 | * 25 | * Value Size Description 26 | * ----- ---- ----------- 27 | * (UnixN) 0x7875 Short tag for this extra block type ("ux") 28 | * TSize Short total data size for this block 29 | * Version 1 byte version of this extra field, currently 1 30 | * UIDSize 1 byte Size of UID field 31 | * UID Variable UID for this entry 32 | * GIDSize 1 byte Size of GID field 33 | * GID Variable GID for this entry 34 | * 35 | * Currently Version is set to the number 1. If there is a need 36 | * to change this field, the version will be incremented. Changes 37 | * may not be backward compatible so this extra field should not be 38 | * used if the version is not recognized. 39 | * 40 | * UIDSize is the size of the UID field in bytes. This size should 41 | * match the size of the UID field on the target OS. 42 | * 43 | * UID is the UID for this entry in standard little endian format. 44 | * 45 | * GIDSize is the size of the GID field in bytes. This size should 46 | * match the size of the GID field on the target OS. 47 | * 48 | * GID is the GID for this entry in standard little endian format. 49 | * 50 | * If both the old 16-bit Unix extra field (tag 0x7855, Info-ZIP Unix) 51 | * and this extra field are present, the values in this extra field 52 | * supercede the values in that extra field. 53 | */ 54 | final class NewUnixExtraField implements ZipExtraField 55 | { 56 | /** @var int header id */ 57 | public const HEADER_ID = 0x7875; 58 | 59 | /** ID of the first non-root user created on a unix system. */ 60 | public const USER_GID_PID = 1000; 61 | 62 | /** @var int version of this extra field, currently 1 */ 63 | private int $version; 64 | 65 | /** @var int User id */ 66 | private int $uid; 67 | 68 | /** @var int Group id */ 69 | private int $gid; 70 | 71 | public function __construct(int $version = 1, int $uid = self::USER_GID_PID, int $gid = self::USER_GID_PID) 72 | { 73 | $this->version = $version; 74 | $this->uid = $uid; 75 | $this->gid = $gid; 76 | } 77 | 78 | /** 79 | * Returns the Header ID (type) of this Extra Field. 80 | * The Header ID is an unsigned short integer (two bytes) 81 | * which must be constant during the life cycle of this object. 82 | */ 83 | public function getHeaderId(): int 84 | { 85 | return self::HEADER_ID; 86 | } 87 | 88 | /** 89 | * Populate data from this array as if it was in local file data. 90 | * 91 | * @param string $buffer the buffer to read data from 92 | * @param ZipEntry|null $entry optional zip entry 93 | * 94 | * @throws ZipException 95 | * 96 | * @return NewUnixExtraField 97 | */ 98 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 99 | { 100 | $length = \strlen($buffer); 101 | 102 | if ($length < 3) { 103 | throw new ZipException(sprintf('X7875_NewUnix length is too short, only %s bytes', $length)); 104 | } 105 | $offset = 0; 106 | [ 107 | 'version' => $version, 108 | 'uidSize' => $uidSize, 109 | ] = unpack('Cversion/CuidSize', $buffer); 110 | $offset += 2; 111 | $gid = self::readSizeIntegerLE(substr($buffer, $offset, $uidSize), $uidSize); 112 | $offset += $uidSize; 113 | $gidSize = unpack('C', $buffer[$offset])[1]; 114 | $offset++; 115 | $uid = self::readSizeIntegerLE(substr($buffer, $offset, $gidSize), $gidSize); 116 | 117 | return new self($version, $gid, $uid); 118 | } 119 | 120 | /** 121 | * Populate data from this array as if it was in central directory data. 122 | * 123 | * @param string $buffer the buffer to read data from 124 | * @param ZipEntry|null $entry optional zip entry 125 | * 126 | * @throws ZipException 127 | * 128 | * @return NewUnixExtraField 129 | */ 130 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 131 | { 132 | return self::unpackLocalFileData($buffer, $entry); 133 | } 134 | 135 | /** 136 | * The actual data to put into local file data - without Header-ID 137 | * or length specifier. 138 | * 139 | * @return string the data 140 | */ 141 | public function packLocalFileData(): string 142 | { 143 | return pack( 144 | 'CCVCV', 145 | $this->version, 146 | 4, // UIDSize 147 | $this->uid, 148 | 4, // GIDSize 149 | $this->gid 150 | ); 151 | } 152 | 153 | /** 154 | * The actual data to put into central directory - without Header-ID or 155 | * length specifier. 156 | * 157 | * @return string the data 158 | */ 159 | public function packCentralDirData(): string 160 | { 161 | return $this->packLocalFileData(); 162 | } 163 | 164 | /** 165 | * @throws ZipException 166 | */ 167 | private static function readSizeIntegerLE(string $data, int $size): int 168 | { 169 | $format = [ 170 | 1 => 'C', // unsigned byte 171 | 2 => 'v', // unsigned short LE 172 | 4 => 'V', // unsigned int LE 173 | ]; 174 | 175 | if (!isset($format[$size])) { 176 | throw new ZipException(sprintf('Invalid size bytes: %d', $size)); 177 | } 178 | 179 | return unpack($format[$size], $data)[1]; 180 | } 181 | 182 | public function getUid(): int 183 | { 184 | return $this->uid; 185 | } 186 | 187 | public function setUid(int $uid): void 188 | { 189 | $this->uid = $uid & 0xFFFFFFFF; 190 | } 191 | 192 | public function getGid(): int 193 | { 194 | return $this->gid; 195 | } 196 | 197 | public function setGid(int $gid): void 198 | { 199 | $this->gid = $gid & 0xFFFFFFFF; 200 | } 201 | 202 | public function getVersion(): int 203 | { 204 | return $this->version; 205 | } 206 | 207 | public function __toString(): string 208 | { 209 | return sprintf( 210 | '0x%04x NewUnix: UID=%d GID=%d', 211 | self::HEADER_ID, 212 | $this->uid, 213 | $this->gid 214 | ); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/NtfsExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Exception\InvalidArgumentException; 15 | use PhpZip\Exception\ZipException; 16 | use PhpZip\Model\Extra\ZipExtraField; 17 | use PhpZip\Model\ZipEntry; 18 | 19 | /** 20 | * NTFS Extra Field. 21 | * 22 | * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification 23 | */ 24 | final class NtfsExtraField implements ZipExtraField 25 | { 26 | /** @var int Header id */ 27 | public const HEADER_ID = 0x000A; 28 | 29 | /** @var int Tag ID */ 30 | public const TIME_ATTR_TAG = 0x0001; 31 | 32 | /** @var int Attribute size */ 33 | public const TIME_ATTR_SIZE = 24; // 3 * 8 34 | 35 | /** 36 | * @var int A file time is a 64-bit value that represents the number of 37 | * 100-nanosecond intervals that have elapsed since 12:00 38 | * A.M. January 1, 1601 Coordinated Universal Time (UTC). 39 | * this is the offset of Windows time 0 to Unix epoch in 100-nanosecond intervals. 40 | */ 41 | public const EPOCH_OFFSET = -116444736000000000; 42 | 43 | /** @var int Modify ntfs time */ 44 | private int $modifyNtfsTime; 45 | 46 | /** @var int Access ntfs time */ 47 | private int $accessNtfsTime; 48 | 49 | /** @var int Create ntfs time */ 50 | private int $createNtfsTime; 51 | 52 | public function __construct(int $modifyNtfsTime, int $accessNtfsTime, int $createNtfsTime) 53 | { 54 | $this->modifyNtfsTime = $modifyNtfsTime; 55 | $this->accessNtfsTime = $accessNtfsTime; 56 | $this->createNtfsTime = $createNtfsTime; 57 | } 58 | 59 | /** 60 | * @return NtfsExtraField 61 | */ 62 | public static function create( 63 | \DateTimeInterface $modifyDateTime, 64 | \DateTimeInterface $accessDateTime, 65 | \DateTimeInterface $createNtfsTime 66 | ): self { 67 | return new self( 68 | self::dateTimeToNtfsTime($modifyDateTime), 69 | self::dateTimeToNtfsTime($accessDateTime), 70 | self::dateTimeToNtfsTime($createNtfsTime) 71 | ); 72 | } 73 | 74 | /** 75 | * Returns the Header ID (type) of this Extra Field. 76 | * The Header ID is an unsigned short integer (two bytes) 77 | * which must be constant during the life cycle of this object. 78 | */ 79 | public function getHeaderId(): int 80 | { 81 | return self::HEADER_ID; 82 | } 83 | 84 | /** 85 | * Populate data from this array as if it was in local file data. 86 | * 87 | * @param string $buffer the buffer to read data from 88 | * @param ZipEntry|null $entry optional zip entry 89 | * 90 | * @throws ZipException 91 | * 92 | * @return NtfsExtraField 93 | */ 94 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 95 | { 96 | if (\PHP_INT_SIZE === 4) { 97 | throw new ZipException('not supported for php-32bit'); 98 | } 99 | 100 | $buffer = substr($buffer, 4); 101 | 102 | $modifyTime = 0; 103 | $accessTime = 0; 104 | $createTime = 0; 105 | 106 | while ($buffer || $buffer !== '') { 107 | [ 108 | 'tag' => $tag, 109 | 'sizeAttr' => $sizeAttr, 110 | ] = unpack('vtag/vsizeAttr', $buffer); 111 | 112 | if ($tag === self::TIME_ATTR_TAG && $sizeAttr === self::TIME_ATTR_SIZE) { 113 | [ 114 | 'modifyTime' => $modifyTime, 115 | 'accessTime' => $accessTime, 116 | 'createTime' => $createTime, 117 | ] = unpack('PmodifyTime/PaccessTime/PcreateTime', substr($buffer, 4, 24)); 118 | 119 | break; 120 | } 121 | $buffer = substr($buffer, 4 + $sizeAttr); 122 | } 123 | 124 | return new self($modifyTime, $accessTime, $createTime); 125 | } 126 | 127 | /** 128 | * Populate data from this array as if it was in central directory data. 129 | * 130 | * @param string $buffer the buffer to read data from 131 | * @param ZipEntry|null $entry optional zip entry 132 | * 133 | * @throws ZipException 134 | * 135 | * @return NtfsExtraField 136 | */ 137 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 138 | { 139 | return self::unpackLocalFileData($buffer, $entry); 140 | } 141 | 142 | /** 143 | * The actual data to put into local file data - without Header-ID 144 | * or length specifier. 145 | * 146 | * @return string the data 147 | */ 148 | public function packLocalFileData(): string 149 | { 150 | return pack( 151 | 'VvvPPP', 152 | 0, 153 | self::TIME_ATTR_TAG, 154 | self::TIME_ATTR_SIZE, 155 | $this->modifyNtfsTime, 156 | $this->accessNtfsTime, 157 | $this->createNtfsTime 158 | ); 159 | } 160 | 161 | public function getModifyNtfsTime(): int 162 | { 163 | return $this->modifyNtfsTime; 164 | } 165 | 166 | public function setModifyNtfsTime(int $modifyNtfsTime): void 167 | { 168 | $this->modifyNtfsTime = $modifyNtfsTime; 169 | } 170 | 171 | public function getAccessNtfsTime(): int 172 | { 173 | return $this->accessNtfsTime; 174 | } 175 | 176 | public function setAccessNtfsTime(int $accessNtfsTime): void 177 | { 178 | $this->accessNtfsTime = $accessNtfsTime; 179 | } 180 | 181 | public function getCreateNtfsTime(): int 182 | { 183 | return $this->createNtfsTime; 184 | } 185 | 186 | public function setCreateNtfsTime(int $createNtfsTime): void 187 | { 188 | $this->createNtfsTime = $createNtfsTime; 189 | } 190 | 191 | /** 192 | * The actual data to put into central directory - without Header-ID or 193 | * length specifier. 194 | * 195 | * @return string the data 196 | */ 197 | public function packCentralDirData(): string 198 | { 199 | return $this->packLocalFileData(); 200 | } 201 | 202 | public function getModifyDateTime(): \DateTimeInterface 203 | { 204 | return self::ntfsTimeToDateTime($this->modifyNtfsTime); 205 | } 206 | 207 | public function setModifyDateTime(\DateTimeInterface $modifyTime): void 208 | { 209 | $this->modifyNtfsTime = self::dateTimeToNtfsTime($modifyTime); 210 | } 211 | 212 | public function getAccessDateTime(): \DateTimeInterface 213 | { 214 | return self::ntfsTimeToDateTime($this->accessNtfsTime); 215 | } 216 | 217 | public function setAccessDateTime(\DateTimeInterface $accessTime): void 218 | { 219 | $this->accessNtfsTime = self::dateTimeToNtfsTime($accessTime); 220 | } 221 | 222 | public function getCreateDateTime(): \DateTimeInterface 223 | { 224 | return self::ntfsTimeToDateTime($this->createNtfsTime); 225 | } 226 | 227 | public function setCreateDateTime(\DateTimeInterface $createTime): void 228 | { 229 | $this->createNtfsTime = self::dateTimeToNtfsTime($createTime); 230 | } 231 | 232 | /** 233 | * @param float $timestamp Float timestamp 234 | */ 235 | public static function timestampToNtfsTime(float $timestamp): int 236 | { 237 | return (int) (($timestamp * 10000000) - self::EPOCH_OFFSET); 238 | } 239 | 240 | public static function dateTimeToNtfsTime(\DateTimeInterface $dateTime): int 241 | { 242 | return self::timestampToNtfsTime((float) $dateTime->format('U.u')); 243 | } 244 | 245 | /** 246 | * @return float Float unix timestamp 247 | */ 248 | public static function ntfsTimeToTimestamp(int $ntfsTime): float 249 | { 250 | return (float) (($ntfsTime + self::EPOCH_OFFSET) / 10000000); 251 | } 252 | 253 | public static function ntfsTimeToDateTime(int $ntfsTime): \DateTimeInterface 254 | { 255 | $timestamp = self::ntfsTimeToTimestamp($ntfsTime); 256 | $dateTime = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6f', $timestamp)); 257 | 258 | if ($dateTime === false) { 259 | throw new InvalidArgumentException('Cannot create date/time object for timestamp ' . $timestamp); 260 | } 261 | 262 | return $dateTime; 263 | } 264 | 265 | public function __toString(): string 266 | { 267 | $args = [self::HEADER_ID]; 268 | $format = '0x%04x NtfsExtra:'; 269 | 270 | if ($this->modifyNtfsTime !== 0) { 271 | $format .= ' Modify:[%s]'; 272 | $args[] = $this->getModifyDateTime()->format(\DATE_ATOM); 273 | } 274 | 275 | if ($this->accessNtfsTime !== 0) { 276 | $format .= ' Access:[%s]'; 277 | $args[] = $this->getAccessDateTime()->format(\DATE_ATOM); 278 | } 279 | 280 | if ($this->createNtfsTime !== 0) { 281 | $format .= ' Create:[%s]'; 282 | $args[] = $this->getCreateDateTime()->format(\DATE_ATOM); 283 | } 284 | 285 | return vsprintf($format, $args); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/OldUnixExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Model\Extra\ZipExtraField; 15 | use PhpZip\Model\ZipEntry; 16 | 17 | /** 18 | * Info-ZIP Unix Extra Field (type 1): 19 | * ==================================. 20 | * 21 | * The following is the layout of the old Info-ZIP extra block for 22 | * Unix. It has been replaced by the extended-timestamp extra block 23 | * (0x5455) and the Unix type 2 extra block (0x7855). 24 | * (Last Revision 19970118) 25 | * 26 | * Local-header version: 27 | * 28 | * Value Size Description 29 | * ----- ---- ----------- 30 | * (Unix1) 0x5855 Short tag for this extra block type ("UX") 31 | * TSize Short total data size for this block 32 | * AcTime Long time of last access (UTC/GMT) 33 | * ModTime Long time of last modification (UTC/GMT) 34 | * UID Short Unix user ID (optional) 35 | * GID Short Unix group ID (optional) 36 | * 37 | * Central-header version: 38 | * 39 | * Value Size Description 40 | * ----- ---- ----------- 41 | * (Unix1) 0x5855 Short tag for this extra block type ("UX") 42 | * TSize Short total data size for this block 43 | * AcTime Long time of last access (GMT/UTC) 44 | * ModTime Long time of last modification (GMT/UTC) 45 | * 46 | * The file access and modification times are in standard Unix signed- 47 | * long format, indicating the number of seconds since 1 January 1970 48 | * 00:00:00. The times are relative to Coordinated Universal Time 49 | * (UTC), also sometimes referred to as Greenwich Mean Time (GMT). To 50 | * convert to local time, the software must know the local timezone 51 | * offset from UTC/GMT. The modification time may be used by non-Unix 52 | * systems to support inter-timezone freshening and updating of zip 53 | * archives. 54 | * 55 | * The local-header extra block may optionally contain UID and GID 56 | * info for the file. The local-header TSize value is the only 57 | * indication of this. Note that Unix UIDs and GIDs are usually 58 | * specific to a particular machine, and they generally require root 59 | * access to restore. 60 | * 61 | * This extra field type is obsolete, but it has been in use since 62 | * mid-1994. Therefore future archiving software should continue to 63 | * support it. 64 | */ 65 | final class OldUnixExtraField implements ZipExtraField 66 | { 67 | /** @var int Header id */ 68 | public const HEADER_ID = 0x5855; 69 | 70 | /** @var int|null Access timestamp */ 71 | private ?int $accessTime; 72 | 73 | /** @var int|null Modify timestamp */ 74 | private ?int $modifyTime; 75 | 76 | /** @var int|null User id */ 77 | private ?int $uid; 78 | 79 | /** @var int|null Group id */ 80 | private ?int $gid; 81 | 82 | public function __construct(?int $accessTime, ?int $modifyTime, ?int $uid, ?int $gid) 83 | { 84 | $this->accessTime = $accessTime; 85 | $this->modifyTime = $modifyTime; 86 | $this->uid = $uid; 87 | $this->gid = $gid; 88 | } 89 | 90 | /** 91 | * Returns the Header ID (type) of this Extra Field. 92 | * The Header ID is an unsigned short integer (two bytes) 93 | * which must be constant during the life cycle of this object. 94 | */ 95 | public function getHeaderId(): int 96 | { 97 | return self::HEADER_ID; 98 | } 99 | 100 | /** 101 | * Populate data from this array as if it was in local file data. 102 | * 103 | * @param string $buffer the buffer to read data from 104 | * @param ZipEntry|null $entry optional zip entry 105 | * 106 | * @return OldUnixExtraField 107 | */ 108 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 109 | { 110 | $length = \strlen($buffer); 111 | 112 | $accessTime = $modifyTime = $uid = $gid = null; 113 | 114 | if ($length >= 4) { 115 | $accessTime = unpack('V', $buffer)[1]; 116 | } 117 | 118 | if ($length >= 8) { 119 | $modifyTime = unpack('V', substr($buffer, 4, 4))[1]; 120 | } 121 | 122 | if ($length >= 10) { 123 | $uid = unpack('v', substr($buffer, 8, 2))[1]; 124 | } 125 | 126 | if ($length >= 12) { 127 | $gid = unpack('v', substr($buffer, 10, 2))[1]; 128 | } 129 | 130 | return new self($accessTime, $modifyTime, $uid, $gid); 131 | } 132 | 133 | /** 134 | * Populate data from this array as if it was in central directory data. 135 | * 136 | * @param string $buffer the buffer to read data from 137 | * @param ZipEntry|null $entry optional zip entry 138 | * 139 | * @return OldUnixExtraField 140 | */ 141 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 142 | { 143 | $length = \strlen($buffer); 144 | 145 | $accessTime = $modifyTime = null; 146 | 147 | if ($length >= 4) { 148 | $accessTime = unpack('V', $buffer)[1]; 149 | } 150 | 151 | if ($length >= 8) { 152 | $modifyTime = unpack('V', substr($buffer, 4, 4))[1]; 153 | } 154 | 155 | return new self($accessTime, $modifyTime, null, null); 156 | } 157 | 158 | /** 159 | * The actual data to put into local file data - without Header-ID 160 | * or length specifier. 161 | * 162 | * @return string the data 163 | */ 164 | public function packLocalFileData(): string 165 | { 166 | $data = ''; 167 | 168 | if ($this->accessTime !== null) { 169 | $data .= pack('V', $this->accessTime); 170 | 171 | if ($this->modifyTime !== null) { 172 | $data .= pack('V', $this->modifyTime); 173 | 174 | if ($this->uid !== null) { 175 | $data .= pack('v', $this->uid); 176 | 177 | if ($this->gid !== null) { 178 | $data .= pack('v', $this->gid); 179 | } 180 | } 181 | } 182 | } 183 | 184 | return $data; 185 | } 186 | 187 | /** 188 | * The actual data to put into central directory - without Header-ID or 189 | * length specifier. 190 | * 191 | * @return string the data 192 | */ 193 | public function packCentralDirData(): string 194 | { 195 | $data = ''; 196 | 197 | if ($this->accessTime !== null) { 198 | $data .= pack('V', $this->accessTime); 199 | 200 | if ($this->modifyTime !== null) { 201 | $data .= pack('V', $this->modifyTime); 202 | } 203 | } 204 | 205 | return $data; 206 | } 207 | 208 | public function getAccessTime(): ?int 209 | { 210 | return $this->accessTime; 211 | } 212 | 213 | public function setAccessTime(?int $accessTime): void 214 | { 215 | $this->accessTime = $accessTime; 216 | } 217 | 218 | public function getAccessDateTime(): ?\DateTimeInterface 219 | { 220 | try { 221 | return $this->accessTime === null ? null 222 | : new \DateTimeImmutable('@' . $this->accessTime); 223 | } catch (\Exception $e) { 224 | return null; 225 | } 226 | } 227 | 228 | public function getModifyTime(): ?int 229 | { 230 | return $this->modifyTime; 231 | } 232 | 233 | public function setModifyTime(?int $modifyTime): void 234 | { 235 | $this->modifyTime = $modifyTime; 236 | } 237 | 238 | public function getModifyDateTime(): ?\DateTimeInterface 239 | { 240 | try { 241 | return $this->modifyTime === null ? null 242 | : new \DateTimeImmutable('@' . $this->modifyTime); 243 | } catch (\Exception $e) { 244 | return null; 245 | } 246 | } 247 | 248 | public function getUid(): ?int 249 | { 250 | return $this->uid; 251 | } 252 | 253 | public function setUid(?int $uid): void 254 | { 255 | $this->uid = $uid; 256 | } 257 | 258 | public function getGid(): ?int 259 | { 260 | return $this->gid; 261 | } 262 | 263 | public function setGid(?int $gid): void 264 | { 265 | $this->gid = $gid; 266 | } 267 | 268 | public function __toString(): string 269 | { 270 | $args = [self::HEADER_ID]; 271 | $format = '0x%04x OldUnix:'; 272 | 273 | if (($modifyTime = $this->getModifyDateTime()) !== null) { 274 | $format .= ' Modify:[%s]'; 275 | $args[] = $modifyTime->format(\DATE_ATOM); 276 | } 277 | 278 | if (($accessTime = $this->getAccessDateTime()) !== null) { 279 | $format .= ' Access:[%s]'; 280 | $args[] = $accessTime->format(\DATE_ATOM); 281 | } 282 | 283 | if ($this->uid !== null) { 284 | $format .= ' UID=%d'; 285 | $args[] = $this->uid; 286 | } 287 | 288 | if ($this->gid !== null) { 289 | $format .= ' GID=%d'; 290 | $args[] = $this->gid; 291 | } 292 | 293 | return vsprintf($format, $args); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/UnicodeCommentExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | /** 15 | * Info-ZIP Unicode Comment Extra Field (0x6375):. 16 | * 17 | * Stores the UTF-8 version of the file comment as stored in the 18 | * central directory header. (Last Revision 20070912) 19 | * 20 | * Value Size Description 21 | * ----- ---- ----------- 22 | * (UCom) 0x6375 Short tag for this extra block type ("uc") 23 | * TSize Short total data size for this block 24 | * Version 1 byte version of this extra field, currently 1 25 | * ComCRC32 4 bytes Comment Field CRC32 Checksum 26 | * UnicodeCom Variable UTF-8 version of the entry comment 27 | * 28 | * Currently Version is set to the number 1. If there is a need 29 | * to change this field, the version will be incremented. Changes 30 | * may not be backward compatible so this extra field should not be 31 | * used if the version is not recognized. 32 | * 33 | * The ComCRC32 is the standard zip CRC32 checksum of the File Comment 34 | * field in the central directory header. This is used to verify that 35 | * the comment field has not changed since the Unicode Comment extra field 36 | * was created. This can happen if a utility changes the File Comment 37 | * field but does not update the UTF-8 Comment extra field. If the CRC 38 | * check fails, this Unicode Comment extra field should be ignored and 39 | * the File Comment field in the header should be used instead. 40 | * 41 | * The UnicodeCom field is the UTF-8 version of the File Comment field 42 | * in the header. As UnicodeCom is defined to be UTF-8, no UTF-8 byte 43 | * order mark (BOM) is used. The length of this field is determined by 44 | * subtracting the size of the previous fields from TSize. If both the 45 | * File Name and Comment fields are UTF-8, the new General Purpose Bit 46 | * Flag, bit 11 (Language encoding flag (EFS)), can be used to indicate 47 | * both the header File Name and Comment fields are UTF-8 and, in this 48 | * case, the Unicode Path and Unicode Comment extra fields are not 49 | * needed and should not be created. Note that, for backward 50 | * compatibility, bit 11 should only be used if the native character set 51 | * of the paths and comments being zipped up are already in UTF-8. It is 52 | * expected that the same file comment storage method, either general 53 | * purpose bit 11 or extra fields, be used in both the Local and Central 54 | * Directory Header for a file. 55 | * 56 | * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.6.8 57 | */ 58 | final class UnicodeCommentExtraField extends AbstractUnicodeExtraField 59 | { 60 | public const HEADER_ID = 0x6375; 61 | 62 | /** 63 | * Returns the Header ID (type) of this Extra Field. 64 | * The Header ID is an unsigned short integer (two bytes) 65 | * which must be constant during the life cycle of this object. 66 | */ 67 | public function getHeaderId(): int 68 | { 69 | return self::HEADER_ID; 70 | } 71 | 72 | public function __toString(): string 73 | { 74 | return sprintf( 75 | '0x%04x UnicodeComment: "%s"', 76 | self::HEADER_ID, 77 | $this->getUnicodeValue() 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/UnicodePathExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | /** 15 | * Info-ZIP Unicode Path Extra Field (0x7075): 16 | * ==========================================. 17 | * 18 | * Stores the UTF-8 version of the file name field as stored in the 19 | * local header and central directory header. (Last Revision 20070912) 20 | * 21 | * Value Size Description 22 | * ----- ---- ----------- 23 | * (UPath) 0x7075 Short tag for this extra block type ("up") 24 | * TSize Short total data size for this block 25 | * Version 1 byte version of this extra field, currently 1 26 | * NameCRC32 4 bytes File Name Field CRC32 Checksum 27 | * UnicodeName Variable UTF-8 version of the entry File Name 28 | * 29 | * Currently Version is set to the number 1. If there is a need 30 | * to change this field, the version will be incremented. Changes 31 | * may not be backward compatible so this extra field should not be 32 | * used if the version is not recognized. 33 | * 34 | * The NameCRC32 is the standard zip CRC32 checksum of the File Name 35 | * field in the header. This is used to verify that the header 36 | * File Name field has not changed since the Unicode Path extra field 37 | * was created. This can happen if a utility renames the File Name but 38 | * does not update the UTF-8 path extra field. If the CRC check fails, 39 | * this UTF-8 Path Extra Field should be ignored and the File Name field 40 | * in the header should be used instead. 41 | * 42 | * The UnicodeName is the UTF-8 version of the contents of the File Name 43 | * field in the header. As UnicodeName is defined to be UTF-8, no UTF-8 44 | * byte order mark (BOM) is used. The length of this field is determined 45 | * by subtracting the size of the previous fields from TSize. If both 46 | * the File Name and Comment fields are UTF-8, the new General Purpose 47 | * Bit Flag, bit 11 (Language encoding flag (EFS)), can be used to 48 | * indicate that both the header File Name and Comment fields are UTF-8 49 | * and, in this case, the Unicode Path and Unicode Comment extra fields 50 | * are not needed and should not be created. Note that, for backward 51 | * compatibility, bit 11 should only be used if the native character set 52 | * of the paths and comments being zipped up are already in UTF-8. It is 53 | * expected that the same file name storage method, either general 54 | * purpose bit 11 or extra fields, be used in both the Local and Central 55 | * Directory Header for a file. 56 | * 57 | * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.6.9 58 | */ 59 | final class UnicodePathExtraField extends AbstractUnicodeExtraField 60 | { 61 | public const HEADER_ID = 0x7075; 62 | 63 | /** 64 | * Returns the Header ID (type) of this Extra Field. 65 | * The Header ID is an unsigned short integer (two bytes) 66 | * which must be constant during the life cycle of this object. 67 | */ 68 | public function getHeaderId(): int 69 | { 70 | return self::HEADER_ID; 71 | } 72 | 73 | public function __toString(): string 74 | { 75 | return sprintf( 76 | '0x%04x UnicodePath: "%s"', 77 | self::HEADER_ID, 78 | $this->getUnicodeValue() 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/UnrecognizedExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Exception\RuntimeException; 15 | use PhpZip\Model\Extra\ZipExtraField; 16 | use PhpZip\Model\ZipEntry; 17 | 18 | /** 19 | * Simple placeholder for all those extra fields we don't want to deal with. 20 | */ 21 | final class UnrecognizedExtraField implements ZipExtraField 22 | { 23 | private int $headerId; 24 | 25 | /** @var string extra field data without Header-ID or length specifier */ 26 | private string $data; 27 | 28 | public function __construct(int $headerId, string $data) 29 | { 30 | $this->headerId = $headerId; 31 | $this->data = $data; 32 | } 33 | 34 | public function setHeaderId(int $headerId): void 35 | { 36 | $this->headerId = $headerId; 37 | } 38 | 39 | /** 40 | * Returns the Header ID (type) of this Extra Field. 41 | * The Header ID is an unsigned short integer (two bytes) 42 | * which must be constant during the life cycle of this object. 43 | */ 44 | public function getHeaderId(): int 45 | { 46 | return $this->headerId; 47 | } 48 | 49 | /** 50 | * Populate data from this array as if it was in local file data. 51 | * 52 | * @param string $buffer the buffer to read data from 53 | * @param ZipEntry|null $entry optional zip entry 54 | * 55 | * @return UnrecognizedExtraField 56 | */ 57 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 58 | { 59 | throw new RuntimeException('Unsupport parse'); 60 | } 61 | 62 | /** 63 | * Populate data from this array as if it was in central directory data. 64 | * 65 | * @param string $buffer the buffer to read data from 66 | * @param ZipEntry|null $entry optional zip entry 67 | * 68 | * @return UnrecognizedExtraField 69 | */ 70 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 71 | { 72 | throw new RuntimeException('Unsupport parse'); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function packLocalFileData(): string 79 | { 80 | return $this->data; 81 | } 82 | 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function packCentralDirData(): string 87 | { 88 | return $this->data; 89 | } 90 | 91 | public function getData(): string 92 | { 93 | return $this->data; 94 | } 95 | 96 | public function setData(string $data): void 97 | { 98 | $this->data = $data; 99 | } 100 | 101 | public function __toString(): string 102 | { 103 | $args = [$this->headerId, $this->data]; 104 | $format = '0x%04x Unrecognized Extra Field: "%s"'; 105 | 106 | return vsprintf($format, $args); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Model/Extra/Fields/Zip64ExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra\Fields; 13 | 14 | use PhpZip\Constants\ZipConstants; 15 | use PhpZip\Exception\RuntimeException; 16 | use PhpZip\Exception\ZipException; 17 | use PhpZip\Model\Extra\ZipExtraField; 18 | use PhpZip\Model\ZipEntry; 19 | 20 | /** 21 | * ZIP64 Extra Field. 22 | * 23 | * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification 24 | */ 25 | final class Zip64ExtraField implements ZipExtraField 26 | { 27 | /** @var int The Header ID for a ZIP64 Extended Information Extra Field. */ 28 | public const HEADER_ID = 0x0001; 29 | 30 | private ?int $uncompressedSize; 31 | 32 | private ?int $compressedSize; 33 | 34 | private ?int $localHeaderOffset; 35 | 36 | private ?int $diskStart; 37 | 38 | public function __construct( 39 | ?int $uncompressedSize = null, 40 | ?int $compressedSize = null, 41 | ?int $localHeaderOffset = null, 42 | ?int $diskStart = null 43 | ) { 44 | $this->uncompressedSize = $uncompressedSize; 45 | $this->compressedSize = $compressedSize; 46 | $this->localHeaderOffset = $localHeaderOffset; 47 | $this->diskStart = $diskStart; 48 | } 49 | 50 | /** 51 | * Returns the Header ID (type) of this Extra Field. 52 | * The Header ID is an unsigned short integer (two bytes) 53 | * which must be constant during the life cycle of this object. 54 | */ 55 | public function getHeaderId(): int 56 | { 57 | return self::HEADER_ID; 58 | } 59 | 60 | /** 61 | * Populate data from this array as if it was in local file data. 62 | * 63 | * @param string $buffer the buffer to read data from 64 | * @param ?ZipEntry $entry 65 | * 66 | * @throws ZipException on error 67 | * 68 | * @return Zip64ExtraField 69 | */ 70 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self 71 | { 72 | $length = \strlen($buffer); 73 | 74 | if ($length === 0) { 75 | // no local file data at all, may happen if an archive 76 | // only holds a ZIP64 extended information extra field 77 | // inside the central directory but not inside the local 78 | // file header 79 | return new self(); 80 | } 81 | 82 | if ($length < 16) { 83 | throw new ZipException( 84 | 'Zip64 extended information must contain both size values in the local file header.' 85 | ); 86 | } 87 | 88 | [ 89 | 'uncompressedSize' => $uncompressedSize, 90 | 'compressedSize' => $compressedSize, 91 | ] = unpack('PuncompressedSize/PcompressedSize', substr($buffer, 0, 16)); 92 | 93 | return new self($uncompressedSize, $compressedSize); 94 | } 95 | 96 | /** 97 | * Populate data from this array as if it was in central directory data. 98 | * 99 | * @param string $buffer the buffer to read data from 100 | * @param ?ZipEntry $entry 101 | * 102 | * @throws ZipException 103 | * 104 | * @return Zip64ExtraField 105 | */ 106 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self 107 | { 108 | if ($entry === null) { 109 | throw new RuntimeException('zipEntry is null'); 110 | } 111 | 112 | $length = \strlen($buffer); 113 | $remaining = $length; 114 | 115 | $uncompressedSize = null; 116 | $compressedSize = null; 117 | $localHeaderOffset = null; 118 | $diskStart = null; 119 | 120 | if ($entry->getUncompressedSize() === ZipConstants::ZIP64_MAGIC) { 121 | if ($remaining < 8) { 122 | throw new ZipException('ZIP64 extension corrupt (no uncompressed size).'); 123 | } 124 | $uncompressedSize = unpack('P', substr($buffer, $length - $remaining, 8))[1]; 125 | $remaining -= 8; 126 | } 127 | 128 | if ($entry->getCompressedSize() === ZipConstants::ZIP64_MAGIC) { 129 | if ($remaining < 8) { 130 | throw new ZipException('ZIP64 extension corrupt (no compressed size).'); 131 | } 132 | $compressedSize = unpack('P', substr($buffer, $length - $remaining, 8))[1]; 133 | $remaining -= 8; 134 | } 135 | 136 | if ($entry->getLocalHeaderOffset() === ZipConstants::ZIP64_MAGIC) { 137 | if ($remaining < 8) { 138 | throw new ZipException('ZIP64 extension corrupt (no relative local header offset).'); 139 | } 140 | $localHeaderOffset = unpack('P', substr($buffer, $length - $remaining, 8))[1]; 141 | $remaining -= 8; 142 | } 143 | 144 | if ($remaining === 4) { 145 | $diskStart = unpack('V', substr($buffer, $length - $remaining, 4))[1]; 146 | } 147 | 148 | return new self($uncompressedSize, $compressedSize, $localHeaderOffset, $diskStart); 149 | } 150 | 151 | /** 152 | * The actual data to put into local file data - without Header-ID 153 | * or length specifier. 154 | * 155 | * @return string the data 156 | */ 157 | public function packLocalFileData(): string 158 | { 159 | if ($this->uncompressedSize !== null || $this->compressedSize !== null) { 160 | if ($this->uncompressedSize === null || $this->compressedSize === null) { 161 | throw new \InvalidArgumentException( 162 | 'Zip64 extended information must contain both size values in the local file header.' 163 | ); 164 | } 165 | 166 | return $this->packSizes(); 167 | } 168 | 169 | return ''; 170 | } 171 | 172 | private function packSizes(): string 173 | { 174 | $data = ''; 175 | 176 | if ($this->uncompressedSize !== null) { 177 | $data .= pack('P', $this->uncompressedSize); 178 | } 179 | 180 | if ($this->compressedSize !== null) { 181 | $data .= pack('P', $this->compressedSize); 182 | } 183 | 184 | return $data; 185 | } 186 | 187 | /** 188 | * The actual data to put into central directory - without Header-ID or 189 | * length specifier. 190 | * 191 | * @return string the data 192 | */ 193 | public function packCentralDirData(): string 194 | { 195 | $data = $this->packSizes(); 196 | 197 | if ($this->localHeaderOffset !== null) { 198 | $data .= pack('P', $this->localHeaderOffset); 199 | } 200 | 201 | if ($this->diskStart !== null) { 202 | $data .= pack('V', $this->diskStart); 203 | } 204 | 205 | return $data; 206 | } 207 | 208 | public function getUncompressedSize(): ?int 209 | { 210 | return $this->uncompressedSize; 211 | } 212 | 213 | public function setUncompressedSize(?int $uncompressedSize): void 214 | { 215 | $this->uncompressedSize = $uncompressedSize; 216 | } 217 | 218 | public function getCompressedSize(): ?int 219 | { 220 | return $this->compressedSize; 221 | } 222 | 223 | public function setCompressedSize(?int $compressedSize): void 224 | { 225 | $this->compressedSize = $compressedSize; 226 | } 227 | 228 | public function getLocalHeaderOffset(): ?int 229 | { 230 | return $this->localHeaderOffset; 231 | } 232 | 233 | public function setLocalHeaderOffset(?int $localHeaderOffset): void 234 | { 235 | $this->localHeaderOffset = $localHeaderOffset; 236 | } 237 | 238 | public function getDiskStart(): ?int 239 | { 240 | return $this->diskStart; 241 | } 242 | 243 | public function setDiskStart(?int $diskStart): void 244 | { 245 | $this->diskStart = $diskStart; 246 | } 247 | 248 | public function __toString(): string 249 | { 250 | $args = [self::HEADER_ID]; 251 | $format = '0x%04x ZIP64: '; 252 | $formats = []; 253 | 254 | if ($this->uncompressedSize !== null) { 255 | $formats[] = 'SIZE=%d'; 256 | $args[] = $this->uncompressedSize; 257 | } 258 | 259 | if ($this->compressedSize !== null) { 260 | $formats[] = 'COMP_SIZE=%d'; 261 | $args[] = $this->compressedSize; 262 | } 263 | 264 | if ($this->localHeaderOffset !== null) { 265 | $formats[] = 'OFFSET=%d'; 266 | $args[] = $this->localHeaderOffset; 267 | } 268 | 269 | if ($this->diskStart !== null) { 270 | $formats[] = 'DISK_START=%d'; 271 | $args[] = $this->diskStart; 272 | } 273 | $format .= implode(' ', $formats); 274 | 275 | return vsprintf($format, $args); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/Model/Extra/ZipExtraDriver.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra; 13 | 14 | use PhpZip\Exception\InvalidArgumentException; 15 | use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField; 16 | use PhpZip\Model\Extra\Fields\AsiExtraField; 17 | use PhpZip\Model\Extra\Fields\ExtendedTimestampExtraField; 18 | use PhpZip\Model\Extra\Fields\JarMarkerExtraField; 19 | use PhpZip\Model\Extra\Fields\NewUnixExtraField; 20 | use PhpZip\Model\Extra\Fields\NtfsExtraField; 21 | use PhpZip\Model\Extra\Fields\OldUnixExtraField; 22 | use PhpZip\Model\Extra\Fields\UnicodeCommentExtraField; 23 | use PhpZip\Model\Extra\Fields\UnicodePathExtraField; 24 | use PhpZip\Model\Extra\Fields\WinZipAesExtraField; 25 | use PhpZip\Model\Extra\Fields\Zip64ExtraField; 26 | 27 | /** 28 | * Class ZipExtraManager. 29 | */ 30 | final class ZipExtraDriver 31 | { 32 | /** 33 | * @var array 34 | * @psalm-var array> 35 | */ 36 | private static array $implementations = [ 37 | ApkAlignmentExtraField::HEADER_ID => ApkAlignmentExtraField::class, 38 | AsiExtraField::HEADER_ID => AsiExtraField::class, 39 | ExtendedTimestampExtraField::HEADER_ID => ExtendedTimestampExtraField::class, 40 | JarMarkerExtraField::HEADER_ID => JarMarkerExtraField::class, 41 | NewUnixExtraField::HEADER_ID => NewUnixExtraField::class, 42 | NtfsExtraField::HEADER_ID => NtfsExtraField::class, 43 | OldUnixExtraField::HEADER_ID => OldUnixExtraField::class, 44 | UnicodeCommentExtraField::HEADER_ID => UnicodeCommentExtraField::class, 45 | UnicodePathExtraField::HEADER_ID => UnicodePathExtraField::class, 46 | WinZipAesExtraField::HEADER_ID => WinZipAesExtraField::class, 47 | Zip64ExtraField::HEADER_ID => Zip64ExtraField::class, 48 | ]; 49 | 50 | private function __construct() 51 | { 52 | } 53 | 54 | /** 55 | * @param string|ZipExtraField $extraField ZipExtraField object or class name 56 | */ 57 | public static function register($extraField): void 58 | { 59 | if (!is_a($extraField, ZipExtraField::class, true)) { 60 | throw new InvalidArgumentException( 61 | sprintf( 62 | '$extraField "%s" is not implements interface %s', 63 | (string) $extraField, 64 | ZipExtraField::class 65 | ) 66 | ); 67 | } 68 | self::$implementations[\call_user_func([$extraField, 'getHeaderId'])] = $extraField; 69 | } 70 | 71 | /** 72 | * @param int|string|ZipExtraField $extraType ZipExtraField object or class name or extra header id 73 | */ 74 | public static function unregister($extraType): bool 75 | { 76 | $headerId = null; 77 | 78 | if (\is_int($extraType)) { 79 | $headerId = $extraType; 80 | } elseif (is_a($extraType, ZipExtraField::class, true)) { 81 | $headerId = \call_user_func([$extraType, 'getHeaderId']); 82 | } else { 83 | return false; 84 | } 85 | 86 | if (isset(self::$implementations[$headerId])) { 87 | unset(self::$implementations[$headerId]); 88 | 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | 95 | public static function getClassNameOrNull(int $headerId): ?string 96 | { 97 | if ($headerId < 0 || $headerId > 0xFFFF) { 98 | throw new \InvalidArgumentException('$headerId out of range: ' . $headerId); 99 | } 100 | 101 | return self::$implementations[$headerId] ?? null; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Model/Extra/ZipExtraField.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model\Extra; 13 | 14 | use PhpZip\Model\ZipEntry; 15 | 16 | /** 17 | * Extra Field in a Local or Central Header of a ZIP archive. 18 | * It defines the common properties of all Extra Fields and how to 19 | * serialize/unserialize them to/from byte arrays. 20 | */ 21 | interface ZipExtraField 22 | { 23 | /** 24 | * Returns the Header ID (type) of this Extra Field. 25 | * The Header ID is an unsigned short integer (two bytes) 26 | * which must be constant during the life cycle of this object. 27 | */ 28 | public function getHeaderId(): int; 29 | 30 | /** 31 | * Populate data from this array as if it was in local file data. 32 | * 33 | * @param string $buffer the buffer to read data from 34 | * @param ZipEntry|null $entry optional zip entry 35 | * 36 | * @return static 37 | */ 38 | public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self; 39 | 40 | /** 41 | * Populate data from this array as if it was in central directory data. 42 | * 43 | * @param string $buffer the buffer to read data from 44 | * @param ZipEntry|null $entry optional zip entry 45 | * 46 | * @return static 47 | */ 48 | public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self; 49 | 50 | /** 51 | * The actual data to put into local file data - without Header-ID 52 | * or length specifier. 53 | * 54 | * @return string the data 55 | */ 56 | public function packLocalFileData(): string; 57 | 58 | /** 59 | * The actual data to put into central directory - without Header-ID or 60 | * length specifier. 61 | * 62 | * @return string the data 63 | */ 64 | public function packCentralDirData(): string; 65 | 66 | public function __toString(): string; 67 | } 68 | -------------------------------------------------------------------------------- /src/Model/ImmutableZipContainer.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model; 13 | 14 | class ImmutableZipContainer implements \Countable 15 | { 16 | /** @var ZipEntry[] */ 17 | protected array $entries; 18 | 19 | /** @var string|null Archive comment */ 20 | protected ?string $archiveComment; 21 | 22 | /** 23 | * @param ZipEntry[] $entries 24 | * @param ?string $archiveComment 25 | */ 26 | public function __construct(array $entries, ?string $archiveComment = null) 27 | { 28 | $this->entries = $entries; 29 | $this->archiveComment = $archiveComment; 30 | } 31 | 32 | /** 33 | * @return ZipEntry[] 34 | */ 35 | public function &getEntries(): array 36 | { 37 | return $this->entries; 38 | } 39 | 40 | public function getArchiveComment(): ?string 41 | { 42 | return $this->archiveComment; 43 | } 44 | 45 | /** 46 | * Count elements of an object. 47 | * 48 | * @see https://php.net/manual/en/countable.count.php 49 | * 50 | * @return int The custom count as an integer. 51 | * The return value is cast to an integer. 52 | */ 53 | public function count(): int 54 | { 55 | return \count($this->entries); 56 | } 57 | 58 | /** 59 | * When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties. 60 | * Any properties that are references to other variables, will remain references. 61 | * Once the cloning is complete, if a __clone() method is defined, 62 | * then the newly created object's __clone() method will be called, to allow any necessary properties that need to 63 | * be changed. NOT CALLABLE DIRECTLY. 64 | * 65 | * @see https://php.net/manual/en/language.oop5.cloning.php 66 | */ 67 | public function __clone() 68 | { 69 | foreach ($this->entries as $key => $value) { 70 | $this->entries[$key] = clone $value; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Model/ZipContainer.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model; 13 | 14 | use PhpZip\Constants\ZipEncryptionMethod; 15 | use PhpZip\Exception\InvalidArgumentException; 16 | use PhpZip\Exception\ZipEntryNotFoundException; 17 | use PhpZip\Exception\ZipException; 18 | 19 | /** 20 | * Zip Container. 21 | */ 22 | class ZipContainer extends ImmutableZipContainer 23 | { 24 | /** 25 | * @var ImmutableZipContainer|null The source container contains zip entries from 26 | * an open zip archive. The source container makes 27 | * it possible to undo changes in the archive. 28 | * When cloning, this container is not cloned. 29 | */ 30 | private ?ImmutableZipContainer $sourceContainer; 31 | 32 | public function __construct(?ImmutableZipContainer $sourceContainer = null) 33 | { 34 | $entries = []; 35 | $archiveComment = null; 36 | 37 | if ($sourceContainer !== null) { 38 | foreach ($sourceContainer->getEntries() as $entryName => $entry) { 39 | $entries[$entryName] = clone $entry; 40 | } 41 | $archiveComment = $sourceContainer->getArchiveComment(); 42 | } 43 | parent::__construct($entries, $archiveComment); 44 | $this->sourceContainer = $sourceContainer; 45 | } 46 | 47 | public function getSourceContainer(): ?ImmutableZipContainer 48 | { 49 | return $this->sourceContainer; 50 | } 51 | 52 | public function addEntry(ZipEntry $entry): void 53 | { 54 | $this->entries[$entry->getName()] = $entry; 55 | } 56 | 57 | /** 58 | * @param string|ZipEntry $entry 59 | */ 60 | public function deleteEntry($entry): bool 61 | { 62 | $entry = $entry instanceof ZipEntry ? $entry->getName() : (string) $entry; 63 | 64 | if (isset($this->entries[$entry])) { 65 | unset($this->entries[$entry]); 66 | 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /** 74 | * @param string|ZipEntry $old 75 | * @param string|ZipEntry $new 76 | * 77 | * @throws ZipException 78 | * 79 | * @return ZipEntry New zip entry 80 | */ 81 | public function renameEntry($old, $new): ZipEntry 82 | { 83 | $old = $old instanceof ZipEntry ? $old->getName() : (string) $old; 84 | $new = $new instanceof ZipEntry ? $new->getName() : (string) $new; 85 | 86 | if (isset($this->entries[$new])) { 87 | throw new InvalidArgumentException('New entry name ' . $new . ' is exists.'); 88 | } 89 | 90 | $entry = $this->getEntry($old); 91 | $newEntry = $entry->rename($new); 92 | 93 | $this->deleteEntry($entry); 94 | $this->addEntry($newEntry); 95 | 96 | return $newEntry; 97 | } 98 | 99 | /** 100 | * @param string|ZipEntry $entryName 101 | * 102 | * @throws ZipEntryNotFoundException 103 | */ 104 | public function getEntry($entryName): ZipEntry 105 | { 106 | $entry = $this->getEntryOrNull($entryName); 107 | 108 | if ($entry !== null) { 109 | return $entry; 110 | } 111 | 112 | throw new ZipEntryNotFoundException($entryName); 113 | } 114 | 115 | /** 116 | * @param string|ZipEntry $entryName 117 | */ 118 | public function getEntryOrNull($entryName): ?ZipEntry 119 | { 120 | $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName; 121 | 122 | return $this->entries[$entryName] ?? null; 123 | } 124 | 125 | /** 126 | * @param string|ZipEntry $entryName 127 | */ 128 | public function hasEntry($entryName): bool 129 | { 130 | $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName; 131 | 132 | return isset($this->entries[$entryName]); 133 | } 134 | 135 | /** 136 | * Delete all entries. 137 | */ 138 | public function deleteAll(): void 139 | { 140 | $this->entries = []; 141 | } 142 | 143 | /** 144 | * Delete entries by regex pattern. 145 | * 146 | * @param string $regexPattern Regex pattern 147 | * 148 | * @return ZipEntry[] Deleted entries 149 | */ 150 | public function deleteByRegex(string $regexPattern): array 151 | { 152 | if (empty($regexPattern)) { 153 | throw new InvalidArgumentException('The regex pattern is not specified'); 154 | } 155 | 156 | /** @var ZipEntry[] $found */ 157 | $found = []; 158 | 159 | foreach ($this->entries as $entryName => $entry) { 160 | if (preg_match($regexPattern, $entryName)) { 161 | $found[] = $entry; 162 | } 163 | } 164 | 165 | foreach ($found as $entry) { 166 | $this->deleteEntry($entry); 167 | } 168 | 169 | return $found; 170 | } 171 | 172 | /** 173 | * Undo all changes done in the archive. 174 | */ 175 | public function unchangeAll(): void 176 | { 177 | $this->entries = []; 178 | 179 | if ($this->sourceContainer !== null) { 180 | foreach ($this->sourceContainer->getEntries() as $entry) { 181 | $this->entries[$entry->getName()] = clone $entry; 182 | } 183 | } 184 | $this->unchangeArchiveComment(); 185 | } 186 | 187 | /** 188 | * Undo change archive comment. 189 | */ 190 | public function unchangeArchiveComment(): void 191 | { 192 | $this->archiveComment = null; 193 | 194 | if ($this->sourceContainer !== null) { 195 | $this->archiveComment = $this->sourceContainer->archiveComment; 196 | } 197 | } 198 | 199 | /** 200 | * Revert all changes done to an entry with the given name. 201 | * 202 | * @param string|ZipEntry $entry Entry name or ZipEntry 203 | */ 204 | public function unchangeEntry($entry): bool 205 | { 206 | $entry = $entry instanceof ZipEntry ? $entry->getName() : (string) $entry; 207 | 208 | if ( 209 | $this->sourceContainer !== null 210 | && isset($this->entries[$entry], $this->sourceContainer->entries[$entry]) 211 | ) { 212 | $this->entries[$entry] = clone $this->sourceContainer->entries[$entry]; 213 | 214 | return true; 215 | } 216 | 217 | return false; 218 | } 219 | 220 | /** 221 | * Entries sort by name. 222 | * 223 | * Example: 224 | * ```php 225 | * $zipContainer->sortByName(static function (string $nameA, string $nameB): int { 226 | * return strcmp($nameA, $nameB); 227 | * }); 228 | * ``` 229 | */ 230 | public function sortByName(callable $cmp): void 231 | { 232 | uksort($this->entries, $cmp); 233 | } 234 | 235 | /** 236 | * Entries sort by entry. 237 | * 238 | * Example: 239 | * ```php 240 | * $zipContainer->sortByEntry(static function (ZipEntry $a, ZipEntry $b): int { 241 | * return strcmp($a->getName(), $b->getName()); 242 | * }); 243 | * ``` 244 | */ 245 | public function sortByEntry(callable $cmp): void 246 | { 247 | uasort($this->entries, $cmp); 248 | } 249 | 250 | public function setArchiveComment(?string $archiveComment): void 251 | { 252 | if ($archiveComment !== null && $archiveComment !== '') { 253 | $length = \strlen($archiveComment); 254 | 255 | if ($length > 0xFFFF) { 256 | throw new InvalidArgumentException('Length comment out of range'); 257 | } 258 | } 259 | $this->archiveComment = $archiveComment; 260 | } 261 | 262 | public function matcher(): ZipEntryMatcher 263 | { 264 | return new ZipEntryMatcher($this); 265 | } 266 | 267 | /** 268 | * Specify a password for extracting files. 269 | * 270 | * @param ?string $password 271 | */ 272 | public function setReadPassword(?string $password): void 273 | { 274 | if ($this->sourceContainer !== null) { 275 | foreach ($this->sourceContainer->entries as $entry) { 276 | if ($entry->isEncrypted()) { 277 | $entry->setPassword($password); 278 | } 279 | } 280 | } 281 | } 282 | 283 | /** 284 | * @throws ZipEntryNotFoundException 285 | * @throws ZipException 286 | */ 287 | public function setReadPasswordEntry(string $entryName, string $password): void 288 | { 289 | if (!isset($this->sourceContainer->entries[$entryName])) { 290 | throw new ZipEntryNotFoundException($entryName); 291 | } 292 | 293 | if ($this->sourceContainer->entries[$entryName]->isEncrypted()) { 294 | $this->sourceContainer->entries[$entryName]->setPassword($password); 295 | } 296 | } 297 | 298 | /** 299 | * @param ?string $writePassword 300 | * 301 | * @throws ZipEntryNotFoundException 302 | */ 303 | public function setWritePassword(?string $writePassword): void 304 | { 305 | $this->matcher()->all()->setPassword($writePassword); 306 | } 307 | 308 | /** 309 | * Remove password. 310 | * 311 | * @throws ZipEntryNotFoundException 312 | */ 313 | public function removePassword(): void 314 | { 315 | $this->matcher()->all()->setPassword(null); 316 | } 317 | 318 | /** 319 | * @param string|ZipEntry $entryName 320 | * 321 | * @throws ZipEntryNotFoundException 322 | */ 323 | public function removePasswordEntry($entryName): void 324 | { 325 | $this->matcher()->add($entryName)->setPassword(null); 326 | } 327 | 328 | /** 329 | * @throws ZipEntryNotFoundException 330 | */ 331 | public function setEncryptionMethod(int $encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256): void 332 | { 333 | $this->matcher()->all()->setEncryptionMethod($encryptionMethod); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/Model/ZipData.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model; 13 | 14 | use PhpZip\Exception\ZipException; 15 | 16 | interface ZipData 17 | { 18 | /** 19 | * @return string returns data as string 20 | */ 21 | public function getDataAsString(): string; 22 | 23 | /** 24 | * @return resource returns stream data 25 | */ 26 | public function getDataAsStream(); 27 | 28 | /** 29 | * @param resource $outStream 30 | * 31 | * @throws ZipException 32 | */ 33 | public function copyDataToStream($outStream); 34 | } 35 | -------------------------------------------------------------------------------- /src/Model/ZipEntryMatcher.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Model; 13 | 14 | use PhpZip\Exception\ZipEntryNotFoundException; 15 | 16 | class ZipEntryMatcher implements \Countable 17 | { 18 | protected ZipContainer $zipContainer; 19 | 20 | protected array $matches = []; 21 | 22 | public function __construct(ZipContainer $zipContainer) 23 | { 24 | $this->zipContainer = $zipContainer; 25 | } 26 | 27 | /** 28 | * @param string|ZipEntry|string[]|ZipEntry[] $entries 29 | * 30 | * @return ZipEntryMatcher 31 | */ 32 | public function add($entries): self 33 | { 34 | $entries = (array) $entries; 35 | $entries = array_map( 36 | static fn ($entry) => $entry instanceof ZipEntry ? $entry->getName() : (string) $entry, 37 | $entries 38 | ); 39 | $this->matches = array_values( 40 | array_map( 41 | 'strval', 42 | array_unique( 43 | array_merge( 44 | $this->matches, 45 | array_keys( 46 | array_intersect_key( 47 | $this->zipContainer->getEntries(), 48 | array_flip($entries) 49 | ) 50 | ) 51 | ) 52 | ) 53 | ) 54 | ); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return ZipEntryMatcher 61 | * @noinspection PhpUnusedParameterInspection 62 | */ 63 | public function match(string $regexp): self 64 | { 65 | array_walk( 66 | $this->zipContainer->getEntries(), 67 | function (ZipEntry $entry, string $entryName) use ($regexp): void { 68 | if (preg_match($regexp, $entryName)) { 69 | $this->matches[] = $entryName; 70 | } 71 | } 72 | ); 73 | $this->matches = array_unique($this->matches); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @return ZipEntryMatcher 80 | */ 81 | public function all(): self 82 | { 83 | $this->matches = array_map( 84 | 'strval', 85 | array_keys($this->zipContainer->getEntries()) 86 | ); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Callable function for all select entries. 93 | * 94 | * Callable function signature: 95 | * function(string $entryName){} 96 | */ 97 | public function invoke(callable $callable): void 98 | { 99 | if (!empty($this->matches)) { 100 | array_walk( 101 | $this->matches, 102 | /** @param string $entryName */ 103 | static function (string $entryName) use ($callable): void { 104 | $callable($entryName); 105 | } 106 | ); 107 | } 108 | } 109 | 110 | public function getMatches(): array 111 | { 112 | return $this->matches; 113 | } 114 | 115 | public function delete(): void 116 | { 117 | array_walk( 118 | $this->matches, 119 | /** @param string $entryName */ 120 | function (string $entryName): void { 121 | $this->zipContainer->deleteEntry($entryName); 122 | } 123 | ); 124 | $this->matches = []; 125 | } 126 | 127 | /** 128 | * @param ?string $password 129 | * @param ?int $encryptionMethod 130 | * 131 | * @throws ZipEntryNotFoundException 132 | */ 133 | public function setPassword(?string $password, ?int $encryptionMethod = null): void 134 | { 135 | array_walk( 136 | $this->matches, 137 | /** @param string $entryName */ 138 | function (string $entryName) use ($password, $encryptionMethod): void { 139 | $entry = $this->zipContainer->getEntry($entryName); 140 | 141 | if (!$entry->isDirectory()) { 142 | $entry->setPassword($password, $encryptionMethod); 143 | } 144 | } 145 | ); 146 | } 147 | 148 | /** 149 | * @throws ZipEntryNotFoundException 150 | */ 151 | public function setEncryptionMethod(int $encryptionMethod): void 152 | { 153 | array_walk( 154 | $this->matches, 155 | /** @param string $entryName */ 156 | function (string $entryName) use ($encryptionMethod): void { 157 | $entry = $this->zipContainer->getEntry($entryName); 158 | 159 | if (!$entry->isDirectory()) { 160 | $entry->setEncryptionMethod($encryptionMethod); 161 | } 162 | } 163 | ); 164 | } 165 | 166 | /** 167 | * @throws ZipEntryNotFoundException 168 | */ 169 | public function disableEncryption(): void 170 | { 171 | array_walk( 172 | $this->matches, 173 | function (string $entryName): void { 174 | $entry = $this->zipContainer->getEntry($entryName); 175 | 176 | if (!$entry->isDirectory()) { 177 | $entry->disableEncryption(); 178 | } 179 | } 180 | ); 181 | } 182 | 183 | /** 184 | * Count elements of an object. 185 | * 186 | * @see http://php.net/manual/en/countable.count.php 187 | * 188 | * @return int the custom count as an integer 189 | */ 190 | public function count(): int 191 | { 192 | return \count($this->matches); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Util/CryptoUtil.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Util; 13 | 14 | use PhpZip\Exception\RuntimeException; 15 | 16 | /** 17 | * Crypto Utils. 18 | * 19 | * @internal 20 | */ 21 | final class CryptoUtil 22 | { 23 | /** 24 | * Decrypt AES-CTR. 25 | * 26 | * @param string $data Encrypted data 27 | * @param string $key Aes key 28 | * @param string $iv Aes IV 29 | * 30 | * @return string Raw data 31 | */ 32 | public static function decryptAesCtr(string $data, string $key, string $iv): string 33 | { 34 | if (\extension_loaded('openssl')) { 35 | $numBits = \strlen($key) * 8; 36 | /** @noinspection PhpComposerExtensionStubsInspection */ 37 | return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, \OPENSSL_RAW_DATA, $iv); 38 | } 39 | 40 | throw new RuntimeException('Openssl extension not loaded'); 41 | } 42 | 43 | /** 44 | * Encrypt AES-CTR. 45 | * 46 | * @param string $data Raw data 47 | * @param string $key Aes key 48 | * @param string $iv Aes IV 49 | * 50 | * @return string Encrypted data 51 | */ 52 | public static function encryptAesCtr(string $data, string $key, string $iv): string 53 | { 54 | if (\extension_loaded('openssl')) { 55 | $numBits = \strlen($key) * 8; 56 | /** @noinspection PhpComposerExtensionStubsInspection */ 57 | return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, \OPENSSL_RAW_DATA, $iv); 58 | } 59 | 60 | throw new RuntimeException('Openssl extension not loaded'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Util/DateTimeConverter.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Util; 13 | 14 | /** 15 | * Convert unix timestamp values to DOS date/time values and vice versa. 16 | * 17 | * The DOS date/time format is a bitmask: 18 | * 19 | * 24 16 8 0 20 | * +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 21 | * |Y|Y|Y|Y|Y|Y|Y|M| |M|M|M|D|D|D|D|D| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s| 22 | * +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 23 | * \___________/\________/\_________/ \________/\____________/\_________/ 24 | * year month day hour minute second 25 | * 26 | * The year is stored as an offset from 1980. 27 | * Seconds are stored in two-second increments. 28 | * (So if the "second" value is 15, it actually represents 30 seconds.) 29 | * 30 | * @see https://docs.microsoft.com/ru-ru/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime?redirectedfrom=MSDN 31 | * 32 | * @internal 33 | */ 34 | class DateTimeConverter 35 | { 36 | /** 37 | * Smallest supported DOS date/time value in a ZIP file, 38 | * which is January 1st, 1980 AD 00:00:00 local time. 39 | * 40 | * @var int 41 | */ 42 | public const MIN_DOS_TIME = (1 << 21) | (1 << 16); 43 | 44 | /** 45 | * Largest supported DOS date/time value in a ZIP file, 46 | * which is December 31st, 2107 AD 23:59:58 local time. 47 | * 48 | * @var int 49 | */ 50 | public const MAX_DOS_TIME = ((2107 - 1980) << 25) | (12 << 21) | (31 << 16) | (23 << 11) | (59 << 5) | (58 >> 1); 51 | 52 | /** 53 | * Convert a 32 bit integer DOS date/time value to a UNIX timestamp value. 54 | * 55 | * @param int $dosTime Dos date/time 56 | * 57 | * @return int Unix timestamp 58 | */ 59 | public static function msDosToUnix(int $dosTime): int 60 | { 61 | if ($dosTime <= self::MIN_DOS_TIME) { 62 | $dosTime = 0; 63 | } elseif ($dosTime > self::MAX_DOS_TIME) { 64 | $dosTime = self::MAX_DOS_TIME; 65 | } 66 | // date_default_timezone_set('UTC'); 67 | return mktime( 68 | (($dosTime >> 11) & 0x1F), // hours 69 | (($dosTime >> 5) & 0x3F), // minutes 70 | (($dosTime << 1) & 0x3E), // seconds 71 | (($dosTime >> 21) & 0x0F), // month 72 | (($dosTime >> 16) & 0x1F), // day 73 | ((($dosTime >> 25) & 0x7F) + 1980) // year 74 | ); 75 | } 76 | 77 | /** 78 | * Converts a UNIX timestamp value to a DOS date/time value. 79 | * 80 | * @param int $unixTimestamp the number of seconds since midnight, January 1st, 81 | * 1970 AD UTC 82 | * 83 | * @return int a DOS date/time value reflecting the local time zone and 84 | * rounded down to even seconds 85 | * and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME 86 | */ 87 | public static function unixToMsDos(int $unixTimestamp): int 88 | { 89 | if ($unixTimestamp < 0) { 90 | throw new \InvalidArgumentException('Negative unix timestamp: ' . $unixTimestamp); 91 | } 92 | 93 | $date = getdate($unixTimestamp); 94 | $dosTime = ( 95 | (($date['year'] - 1980) << 25) 96 | | ($date['mon'] << 21) 97 | | ($date['mday'] << 16) 98 | | ($date['hours'] << 11) 99 | | ($date['minutes'] << 5) 100 | | ($date['seconds'] >> 1) 101 | ); 102 | 103 | if ($dosTime <= self::MIN_DOS_TIME) { 104 | $dosTime = 0; 105 | } 106 | 107 | return $dosTime; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Util/FileAttribUtil.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Util; 13 | 14 | use PhpZip\Constants\DosAttrs; 15 | use PhpZip\Constants\UnixStat; 16 | 17 | /** 18 | * @internal 19 | */ 20 | class FileAttribUtil implements DosAttrs, UnixStat 21 | { 22 | /** 23 | * Get DOS mode. 24 | */ 25 | public static function getDosMode(int $xattr): string 26 | { 27 | $mode = (($xattr & self::DOS_DIRECTORY) === self::DOS_DIRECTORY) ? 'd' : '-'; 28 | $mode .= (($xattr & self::DOS_ARCHIVE) === self::DOS_ARCHIVE) ? 'a' : '-'; 29 | $mode .= (($xattr & self::DOS_READ_ONLY) === self::DOS_READ_ONLY) ? 'r' : '-'; 30 | $mode .= (($xattr & self::DOS_HIDDEN) === self::DOS_HIDDEN) ? 'h' : '-'; 31 | $mode .= (($xattr & self::DOS_SYSTEM) === self::DOS_SYSTEM) ? 's' : '-'; 32 | $mode .= (($xattr & self::DOS_LABEL) === self::DOS_LABEL) ? 'l' : '-'; 33 | 34 | return $mode; 35 | } 36 | 37 | /** 38 | * @noinspection DuplicatedCode 39 | */ 40 | public static function getUnixMode(int $permission): string 41 | { 42 | $mode = ''; 43 | switch ($permission & self::UNX_IFMT) { 44 | case self::UNX_IFDIR: 45 | $mode .= 'd'; 46 | break; 47 | 48 | case self::UNX_IFREG: 49 | $mode .= '-'; 50 | break; 51 | 52 | case self::UNX_IFLNK: 53 | $mode .= 'l'; 54 | break; 55 | 56 | case self::UNX_IFBLK: 57 | $mode .= 'b'; 58 | break; 59 | 60 | case self::UNX_IFCHR: 61 | $mode .= 'c'; 62 | break; 63 | 64 | case self::UNX_IFIFO: 65 | $mode .= 'p'; 66 | break; 67 | 68 | case self::UNX_IFSOCK: 69 | $mode .= 's'; 70 | break; 71 | 72 | default: 73 | $mode .= '?'; 74 | break; 75 | } 76 | $mode .= ($permission & self::UNX_IRUSR) ? 'r' : '-'; 77 | $mode .= ($permission & self::UNX_IWUSR) ? 'w' : '-'; 78 | 79 | if ($permission & self::UNX_IXUSR) { 80 | $mode .= ($permission & self::UNX_ISUID) ? 's' : 'x'; 81 | } else { 82 | $mode .= ($permission & self::UNX_ISUID) ? 'S' : '-'; // S==undefined 83 | } 84 | $mode .= ($permission & self::UNX_IRGRP) ? 'r' : '-'; 85 | $mode .= ($permission & self::UNX_IWGRP) ? 'w' : '-'; 86 | 87 | if ($permission & self::UNX_IXGRP) { 88 | $mode .= ($permission & self::UNX_ISGID) ? 's' : 'x'; 89 | } // == self::UNX_ENFMT 90 | else { 91 | $mode .= ($permission & self::UNX_ISGID) ? 'S' : '-'; 92 | } // SunOS 4.1.x 93 | 94 | $mode .= ($permission & self::UNX_IROTH) ? 'r' : '-'; 95 | $mode .= ($permission & self::UNX_IWOTH) ? 'w' : '-'; 96 | 97 | if ($permission & self::UNX_IXOTH) { 98 | $mode .= ($permission & self::UNX_ISVTX) ? 't' : 'x'; 99 | } // "sticky bit" 100 | else { 101 | $mode .= ($permission & self::UNX_ISVTX) ? 'T' : '-'; 102 | } // T==undefined 103 | 104 | return $mode; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Util/Iterator/IgnoreFilesFilterIterator.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Util\Iterator; 13 | 14 | use PhpZip\Util\StringUtil; 15 | 16 | /** 17 | * Iterator for ignore files. 18 | */ 19 | class IgnoreFilesFilterIterator extends \FilterIterator 20 | { 21 | /** Ignore list files. */ 22 | private array $ignoreFiles = ['..']; 23 | 24 | public function __construct(\Iterator $iterator, array $ignoreFiles) 25 | { 26 | parent::__construct($iterator); 27 | $this->ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles); 28 | } 29 | 30 | /** 31 | * Check whether the current element of the iterator is acceptable. 32 | * 33 | * @see http://php.net/manual/en/filteriterator.accept.php 34 | * 35 | * @return bool true if the current element is acceptable, otherwise false 36 | */ 37 | public function accept(): bool 38 | { 39 | /** 40 | * @var \SplFileInfo $fileInfo 41 | */ 42 | $fileInfo = $this->current(); 43 | $pathname = str_replace('\\', '/', $fileInfo->getPathname()); 44 | 45 | foreach ($this->ignoreFiles as $ignoreFile) { 46 | // handler dir and sub dir 47 | if ($fileInfo->isDir() 48 | && StringUtil::endsWith($ignoreFile, '/') 49 | && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) 50 | ) { 51 | return false; 52 | } 53 | 54 | // handler filename 55 | if (StringUtil::endsWith($pathname, $ignoreFile)) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Util\Iterator; 13 | 14 | use PhpZip\Util\StringUtil; 15 | 16 | /** 17 | * Recursive iterator for ignore files. 18 | */ 19 | class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator 20 | { 21 | /** Ignore list files. */ 22 | private array $ignoreFiles = ['..']; 23 | 24 | public function __construct(\RecursiveIterator $iterator, array $ignoreFiles) 25 | { 26 | parent::__construct($iterator); 27 | $this->ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles); 28 | } 29 | 30 | /** 31 | * Check whether the current element of the iterator is acceptable. 32 | * 33 | * @see http://php.net/manual/en/filteriterator.accept.php 34 | * 35 | * @return bool true if the current element is acceptable, otherwise false 36 | */ 37 | public function accept(): bool 38 | { 39 | /** 40 | * @var \SplFileInfo $fileInfo 41 | */ 42 | $fileInfo = $this->current(); 43 | $pathname = str_replace('\\', '/', $fileInfo->getPathname()); 44 | 45 | foreach ($this->ignoreFiles as $ignoreFile) { 46 | // handler dir and sub dir 47 | if ($fileInfo->isDir() 48 | && $ignoreFile[\strlen($ignoreFile) - 1] === '/' 49 | && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) 50 | ) { 51 | return false; 52 | } 53 | 54 | // handler filename 55 | if (StringUtil::endsWith($pathname, $ignoreFile)) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | 63 | /** 64 | * @return IgnoreFilesRecursiveFilterIterator 65 | * @psalm-suppress UndefinedInterfaceMethod 66 | * @noinspection PhpPossiblePolymorphicInvocationInspection 67 | */ 68 | public function getChildren(): self 69 | { 70 | return new self($this->getInnerIterator()->getChildren(), $this->ignoreFiles); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Util/MathUtil.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Util; 13 | 14 | /** 15 | * Math util. 16 | * 17 | * @internal 18 | */ 19 | final class MathUtil 20 | { 21 | /** 22 | * Cast to signed int 32-bit. 23 | */ 24 | public static function toSignedInt32(int $int): int 25 | { 26 | if (\PHP_INT_SIZE === 8) { 27 | $int &= 0xFFFFFFFF; 28 | 29 | if ($int & 0x80000000) { 30 | return $int - 0x100000000; 31 | } 32 | } 33 | 34 | return $int; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Util/StringUtil.php: -------------------------------------------------------------------------------- 1 | 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PhpZip\Util; 13 | 14 | /** 15 | * String Util. 16 | * 17 | * @internal 18 | */ 19 | final class StringUtil 20 | { 21 | public static function endsWith(string $haystack, string $needle): bool 22 | { 23 | return $needle === '' || ($haystack !== '' && substr_compare($haystack, $needle, -\strlen($needle)) === 0); 24 | } 25 | 26 | public static function isBinary(string $string): bool 27 | { 28 | return strpos($string, "\0") !== false; 29 | } 30 | 31 | public static function isASCII(string $name): bool 32 | { 33 | return preg_match('~[^\x20-\x7e]~', $name) === 0; 34 | } 35 | } 36 | --------------------------------------------------------------------------------