Release of v5.1.1-alpha1

Move all banners to GitHub. Adds library phpspreadsheet to JCB. Adds import item example to demo component. Updates the Superpower class with the GetRemote class in the plugin. Ensures the super power autoloader triggers the correct repositories.
This commit is contained in:
2025-03-04 21:50:18 +00:00
parent 442263e387
commit 06185f8c3a
1141 changed files with 193033 additions and 158 deletions

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
trait Assertions
{
protected function assertFileContains(string $filePath, string $needle): void
{
$last = '';
$handle = fopen($filePath, 'r');
while (!feof($handle)) {
$line = fgets($handle, 1024);
if(str_contains($last . $line, $needle)) {
fclose($handle);
return;
}
$last = $line;
}
fclose($handle);
$this->fail("File {$filePath} must contain {$needle}");
}
protected function assertFileDoesNotContain(string $filePath, string $needle): void
{
$last = '';
$handle = fopen($filePath, 'r');
while (!feof($handle)) {
$line = fgets($handle, 1024);
if(str_contains($last . $line, $needle)) {
fclose($handle);
$this->fail("File {$filePath} must not contain {$needle}");
}
$last = $line;
}
fclose($handle);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ZipStream\CentralDirectoryFileHeader;
use ZipStream\CompressionMethod;
class CentralDirectoryFileHeaderTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z');
$header = CentralDirectoryFileHeader::generate(
versionMadeBy: 0x603,
versionNeededToExtract: 0x002D,
generalPurposeBitFlag: 0x2222,
compressionMethod: CompressionMethod::DEFLATE,
lastModificationDateTime: $dateTime,
crc32: 0x11111111,
compressedSize: 0x77777777,
uncompressedSize: 0x99999999,
fileName: 'test.png',
extraField: 'some content',
fileComment: 'some comment',
diskNumberStart: 0,
internalFileAttributes: 0,
externalFileAttributes: 32,
relativeOffsetOfLocalHeader: 0x1234,
);
$this->assertSame(
bin2hex($header),
'504b0102' . // 4 bytes; central file header signature
'0306' . // 2 bytes; version made by
'2d00' . // 2 bytes; version needed to extract
'2222' . // 2 bytes; general purpose bit flag
'0800' . // 2 bytes; compression method
'2008' . // 2 bytes; last mod file time
'2154' . // 2 bytes; last mod file date
'11111111' . // 4 bytes; crc-32
'77777777' . // 4 bytes; compressed size
'99999999' . // 4 bytes; uncompressed size
'0800' . // 2 bytes; file name length (n)
'0c00' . // 2 bytes; extra field length (m)
'0c00' . // 2 bytes; file comment length (o)
'0000' . // 2 bytes; disk number start
'0000' . // 2 bytes; internal file attributes
'20000000' . // 4 bytes; external file attributes
'34120000' . // 4 bytes; relative offset of local header
'746573742e706e67' . // n bytes; file name
'736f6d6520636f6e74656e74' . // m bytes; extra field
'736f6d6520636f6d6d656e74' // o bytes; file comment
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use PHPUnit\Framework\TestCase;
use ZipStream\DataDescriptor;
class DataDescriptorTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$this->assertSame(
bin2hex(DataDescriptor::generate(
crc32UncompressedData: 0x11111111,
compressedSize: 0x77777777,
uncompressedSize: 0x99999999,
)),
'504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50
'11111111' . // 4 bytes; CRC-32 of uncompressed data
'77777777' . // 4 bytes; Compressed size
'99999999' // 4 bytes; Uncompressed size
);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use PHPUnit\Framework\TestCase;
use ZipStream\EndOfCentralDirectory;
class EndOfCentralDirectoryTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$this->assertSame(
bin2hex(EndOfCentralDirectory::generate(
numberOfThisDisk: 0x00,
numberOfTheDiskWithCentralDirectoryStart: 0x00,
numberOfCentralDirectoryEntriesOnThisDisk: 0x10,
numberOfCentralDirectoryEntries: 0x10,
sizeOfCentralDirectory: 0x22,
centralDirectoryStartOffsetOnDisk: 0x33,
zipFileComment: 'foo',
)),
'504b0506' . // 4 bytes; end of central dir signature 0x06054b50
'0000' . // 2 bytes; number of this disk
'0000' . // 2 bytes; number of the disk with the start of the central directory
'1000' . // 2 bytes; total number of entries in the central directory on this disk
'1000' . // 2 bytes; total number of entries in the central directory
'22000000' . // 4 bytes; size of the central directory
'33000000' . // 4 bytes; offset of start of central directory with respect to the starting disk number
'0300' . // 2 bytes; .ZIP file comment length
bin2hex('foo')
);
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
class EndlessCycleStream implements StreamInterface
{
private int $offset = 0;
public function __construct(private readonly string $toRepeat = '0')
{
}
public function __toString(): string
{
throw new RuntimeException('Infinite Stream!');
}
public function close(): void
{
$this->detach();
}
/**
* @return null
*/
public function detach()
{
return;
}
public function getSize(): ?int
{
return null;
}
public function tell(): int
{
return $this->offset;
}
public function eof(): bool
{
return false;
}
public function isSeekable(): bool
{
return true;
}
public function seek(int $offset, int $whence = SEEK_SET): void
{
switch($whence) {
case SEEK_SET:
$this->offset = $offset;
break;
case SEEK_CUR:
$this->offset += $offset;
break;
case SEEK_END:
throw new RuntimeException('Infinite Stream!');
break;
}
}
public function rewind(): void
{
$this->seek(0);
}
public function isWritable(): bool
{
return false;
}
public function write(string $string): int
{
throw new RuntimeException('Not writeable');
}
public function isReadable(): bool
{
return true;
}
public function read(int $length): string
{
$this->offset += $length;
return substr(str_repeat($this->toRepeat, (int) ceil($length / strlen($this->toRepeat))), 0, $length);
}
public function getContents(): string
{
throw new RuntimeException('Infinite Stream!');
}
public function getMetadata(?string $key = null): array|null
{
return $key !== null ? null : [];
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
class FaultInjectionResource
{
public const NAME = 'zipstream-php-test-broken-resource';
/** @var resource */
public $context;
private array $injectFaults;
private string $mode;
/**
* @return resource
*/
public static function getResource(array $injectFaults)
{
self::register();
return fopen(self::NAME . '://foobar', 'rw+', false, self::createStreamContext($injectFaults));
}
public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool
{
$options = stream_context_get_options($this->context);
if (!isset($options[self::NAME]['injectFaults'])) {
return false;
}
$this->mode = $mode;
$this->injectFaults = $options[self::NAME]['injectFaults'];
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return true;
}
public function stream_write(string $data)
{
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return true;
}
public function stream_eof()
{
return true;
}
public function stream_seek(int $offset, int $whence): bool
{
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return true;
}
public function stream_tell(): int
{
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return 0;
}
public static function register(): void
{
if (!in_array(self::NAME, stream_get_wrappers(), true)) {
stream_wrapper_register(self::NAME, __CLASS__);
}
}
public function stream_stat(): array
{
static $modeMap = [
'r' => 33060,
'rb' => 33060,
'r+' => 33206,
'w' => 33188,
'wb' => 33188,
];
return [
'dev' => 0,
'ino' => 0,
'mode' => $modeMap[$this->mode],
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => 0,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
];
}
public function url_stat(string $path, int $flags): array
{
return [
'dev' => 0,
'ino' => 0,
'mode' => 0,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => 0,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
];
}
private static function createStreamContext(array $injectFaults)
{
return stream_context_create([
self::NAME => ['injectFaults' => $injectFaults],
]);
}
private function shouldFail(string $function): bool
{
return in_array($function, $this->injectFaults, true);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ZipStream\CompressionMethod;
use ZipStream\LocalFileHeader;
class LocalFileHeaderTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z');
$header = LocalFileHeader::generate(
versionNeededToExtract: 0x002D,
generalPurposeBitFlag: 0x2222,
compressionMethod: CompressionMethod::DEFLATE,
lastModificationDateTime: $dateTime,
crc32UncompressedData: 0x11111111,
compressedSize: 0x77777777,
uncompressedSize: 0x99999999,
fileName: 'test.png',
extraField: 'some content'
);
$this->assertSame(
bin2hex((string) $header),
'504b0304' . // 4 bytes; Local file header signature
'2d00' . // 2 bytes; Version needed to extract (minimum)
'2222' . // 2 bytes; General purpose bit flag
'0800' . // 2 bytes; Compression method; e.g. none = 0, DEFLATE = 8
'2008' . // 2 bytes; File last modification time
'2154' . // 2 bytes; File last modification date
'11111111' . // 4 bytes; CRC-32 of uncompressed data
'77777777' . // 4 bytes; Compressed size (or 0xffffffff for ZIP64)
'99999999' . // 4 bytes; Uncompressed size (or 0xffffffff for ZIP64)
'0800' . // 2 bytes; File name length (n)
'0c00' . // 2 bytes; Extra field length (m)
'746573742e706e67' . // n bytes; File name
'736f6d6520636f6e74656e74' // m bytes; Extra field
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use ZipStream\PackField;
class PackFieldTest extends TestCase
{
public function testPacksFields(): void
{
$this->assertSame(
bin2hex(PackField::pack(new PackField(format: 'v', value: 0x1122))),
'2211',
);
}
public function testOverflow2(): void
{
$this->expectException(RuntimeException::class);
PackField::pack(new PackField(format: 'v', value: 0xFFFFF));
}
public function testOverflow4(): void
{
$this->expectException(RuntimeException::class);
PackField::pack(new PackField(format: 'V', value: 0xFFFFFFFFF));
}
public function testUnknownOperator(): void
{
$this->assertSame(
bin2hex(PackField::pack(new PackField(format: 'a', value: 0x1122))),
'34',
);
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* @internal
*/
class ResourceStream implements StreamInterface
{
public function __construct(
/**
* @var resource
*/
private $stream
) {
}
public function __toString(): string
{
if ($this->isSeekable()) {
$this->seek(0);
}
return (string) stream_get_contents($this->stream);
}
public function close(): void
{
$stream = $this->detach();
if ($stream) {
fclose($stream);
}
}
public function detach()
{
$result = $this->stream;
// According to the interface, the stream is left in an unusable state;
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
$this->stream = null;
return $result;
}
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!$this->isSeekable()) {
throw new RuntimeException();
}
if (fseek($this->stream, $offset, $whence) !== 0) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
}
public function isSeekable(): bool
{
return (bool)$this->getMetadata('seekable');
}
public function getMetadata(?string $key = null)
{
$metadata = stream_get_meta_data($this->stream);
return $key !== null ? @$metadata[$key] : $metadata;
}
public function getSize(): ?int
{
$stats = fstat($this->stream);
return $stats['size'];
}
public function tell(): int
{
$position = ftell($this->stream);
if ($position === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return $position;
}
public function eof(): bool
{
return feof($this->stream);
}
public function rewind(): void
{
$this->seek(0);
}
public function write(string $string): int
{
if (!$this->isWritable()) {
throw new RuntimeException();
}
if (fwrite($this->stream, $string) === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return strlen($string);
}
public function isWritable(): bool
{
$mode = $this->getMetadata('mode');
if (!is_string($mode)) {
// @codeCoverageIgnoreStart
throw new RuntimeException('Could not get stream mode from metadata!');
// @codeCoverageIgnoreEnd
}
return preg_match('/[waxc+]/', $mode) === 1;
}
public function read(int $length): string
{
if (!$this->isReadable()) {
throw new RuntimeException();
}
$result = fread($this->stream, $length);
if ($result === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return $result;
}
public function isReadable(): bool
{
$mode = $this->getMetadata('mode');
if (!is_string($mode)) {
// @codeCoverageIgnoreStart
throw new RuntimeException('Could not get stream mode from metadata!');
// @codeCoverageIgnoreEnd
}
return preg_match('/[r+]/', $mode) === 1;
}
public function getContents(): string
{
if (!$this->isReadable()) {
throw new RuntimeException();
}
$result = stream_get_contents($this->stream);
if ($result === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return $result;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ZipStream\Exception\DosTimeOverflowException;
use ZipStream\Time;
class TimeTest extends TestCase
{
public function testNormalDateToDosTime(): void
{
$this->assertSame(
Time::dateTimeToDosTime(new DateTimeImmutable('2014-11-17T17:46:08Z')),
1165069764
);
// January 1 1980 - DOS Epoch.
$this->assertSame(
Time::dateTimeToDosTime(new DateTimeImmutable('1980-01-01T00:00:00+00:00')),
2162688
);
}
public function testTooEarlyDateToDosTime(): void
{
$this->expectException(DosTimeOverflowException::class);
// January 1 1980 is the minimum DOS Epoch.
Time::dateTimeToDosTime(new DateTimeImmutable('1970-01-01T00:00:00+00:00'));
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use function fgets;
use function pclose;
use function popen;
use function preg_match;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use function strtolower;
use ZipArchive;
trait Util
{
protected function getTmpFileStream(): array
{
$tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest');
$stream = fopen($tmp, 'wb+');
return [$tmp, $stream];
}
protected function cmdExists(string $command): bool
{
if (strtolower(\substr(PHP_OS, 0, 3)) === 'win') {
$fp = popen("where $command", 'r');
$result = fgets($fp, 255);
$exists = !preg_match('#Could not find files#', $result);
pclose($fp);
} else { // non-Windows
$fp = popen("which $command", 'r');
$result = fgets($fp, 255);
$exists = !empty($result);
pclose($fp);
}
return $exists;
}
protected function dumpZipContents(string $path): string
{
if (!$this->cmdExists('hexdump')) {
return '';
}
$output = [];
if (!exec("hexdump -C \"$path\" | head -n 50", $output)) {
return '';
}
return "\nHexdump:\n" . implode("\n", $output);
}
protected function validateAndExtractZip(string $zipPath): string
{
$tmpDir = $this->getTmpDir();
$zipArchive = new ZipArchive();
$result = $zipArchive->open($zipPath);
if ($result !== true) {
$codeName = $this->zipArchiveOpenErrorCodeName($result);
$debugInformation = $this->dumpZipContents($zipPath);
$this->fail("Failed to open {$zipPath}. Code: $result ($codeName)$debugInformation");
return $tmpDir;
}
$this->assertSame(0, $zipArchive->status);
$this->assertSame(0, $zipArchive->statusSys);
$zipArchive->extractTo($tmpDir);
$zipArchive->close();
return $tmpDir;
}
protected function zipArchiveOpenErrorCodeName(int $code): string
{
switch($code) {
case ZipArchive::ER_EXISTS: return 'ER_EXISTS';
case ZipArchive::ER_INCONS: return 'ER_INCONS';
case ZipArchive::ER_INVAL: return 'ER_INVAL';
case ZipArchive::ER_MEMORY: return 'ER_MEMORY';
case ZipArchive::ER_NOENT: return 'ER_NOENT';
case ZipArchive::ER_NOZIP: return 'ER_NOZIP';
case ZipArchive::ER_OPEN: return 'ER_OPEN';
case ZipArchive::ER_READ: return 'ER_READ';
case ZipArchive::ER_SEEK: return 'ER_SEEK';
default: return 'unknown';
}
}
protected function getTmpDir(): string
{
$tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest');
unlink($tmp);
mkdir($tmp) or $this->fail('Failed to make directory');
return $tmp;
}
/**
* @return string[]
*/
protected function getRecursiveFileList(string $path, bool $includeDirectories = false): array
{
$data = [];
$path = (string)realpath($path);
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
$pathLen = strlen($path);
foreach ($files as $file) {
$filePath = $file->getRealPath();
if (is_dir($filePath) && !$includeDirectories) {
continue;
}
$data[] = substr($filePath, $pathLen + 1);
}
sort($data);
return $data;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test\Zip64;
use PHPUnit\Framework\TestCase;
use ZipStream\Zip64\DataDescriptor;
class DataDescriptorTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$descriptor = DataDescriptor::generate(
crc32UncompressedData: 0x11111111,
compressedSize: (0x77777777 << 32) + 0x66666666,
uncompressedSize: (0x99999999 << 32) + 0x88888888,
);
$this->assertSame(
bin2hex($descriptor),
'504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50
'11111111' . // 4 bytes; CRC-32 of uncompressed data
'6666666677777777' . // 8 bytes; Compressed size
'8888888899999999' // 8 bytes; Uncompressed size
);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test\Zip64;
use PHPUnit\Framework\TestCase;
use ZipStream\Zip64\EndOfCentralDirectoryLocator;
class EndOfCentralDirectoryLocatorTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$descriptor = EndOfCentralDirectoryLocator::generate(
numberOfTheDiskWithZip64CentralDirectoryStart: 0x11111111,
zip64centralDirectoryStartOffsetOnDisk: (0x22222222 << 32) + 0x33333333,
totalNumberOfDisks: 0x44444444,
);
$this->assertSame(
bin2hex($descriptor),
'504b0607' . // 4 bytes; zip64 end of central dir locator signature - 0x07064b50
'11111111' . // 4 bytes; number of the disk with the start of the zip64 end of central directory
'3333333322222222' . // 28 bytes; relative offset of the zip64 end of central directory record
'44444444' // 4 bytes;total number of disks
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test\Zip64;
use PHPUnit\Framework\TestCase;
use ZipStream\Zip64\EndOfCentralDirectory;
class EndOfCentralDirectoryTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$descriptor = EndOfCentralDirectory::generate(
versionMadeBy: 0x3333,
versionNeededToExtract: 0x4444,
numberOfThisDisk: 0x55555555,
numberOfTheDiskWithCentralDirectoryStart: 0x66666666,
numberOfCentralDirectoryEntriesOnThisDisk: (0x77777777 << 32) + 0x88888888,
numberOfCentralDirectoryEntries: (0x99999999 << 32) + 0xAAAAAAAA,
sizeOfCentralDirectory: (0xBBBBBBBB << 32) + 0xCCCCCCCC,
centralDirectoryStartOffsetOnDisk: (0xDDDDDDDD << 32) + 0xEEEEEEEE,
extensibleDataSector: 'foo',
);
$this->assertSame(
bin2hex($descriptor),
'504b0606' . // 4 bytes;zip64 end of central dir signature - 0x06064b50
'2f00000000000000' . // 8 bytes; size of zip64 end of central directory record
'3333' . // 2 bytes; version made by
'4444' . // 2 bytes; version needed to extract
'55555555' . // 4 bytes; number of this disk
'66666666' . // 4 bytes; number of the disk with the start of the central directory
'8888888877777777' . // 8 bytes; total number of entries in the central directory on this disk
'aaaaaaaa99999999' . // 8 bytes; total number of entries in the central directory
'ccccccccbbbbbbbb' . // 8 bytes; size of the central directory
'eeeeeeeedddddddd' . // 8 bytes; offset of start of central directory with respect to the starting disk number
bin2hex('foo')
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test\Zip64;
use PHPUnit\Framework\TestCase;
use ZipStream\Zip64\ExtendedInformationExtraField;
class ExtendedInformationExtraFieldTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$extraField = ExtendedInformationExtraField::generate(
originalSize: (0x77777777 << 32) + 0x66666666,
compressedSize: (0x99999999 << 32) + 0x88888888,
relativeHeaderOffset: (0x22222222 << 32) + 0x11111111,
diskStartNumber: 0x33333333,
);
$this->assertSame(
bin2hex($extraField),
'0100' . // 2 bytes; Tag for this "extra" block type
'1c00' . // 2 bytes; Size of this "extra" block
'6666666677777777' . // 8 bytes; Original uncompressed file size
'8888888899999999' . // 8 bytes; Size of compressed data
'1111111122222222' . // 8 bytes; Offset of local header record
'33333333' // 4 bytes; Number of the disk on which this file starts
);
}
public function testSerializesEmptyCorrectly(): void
{
$extraField = ExtendedInformationExtraField::generate();
$this->assertSame(
bin2hex($extraField),
'0100' . // 2 bytes; Tag for this "extra" block type
'0000' // 2 bytes; Size of this "extra" block
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test\Zs;
use PHPUnit\Framework\TestCase;
use ZipStream\Zs\ExtendedInformationExtraField;
class ExtendedInformationExtraFieldTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$extraField = ExtendedInformationExtraField::generate();
$this->assertSame(
bin2hex((string) $extraField),
'5356' . // 2 bytes; Tag for this "extra" block type
'0000' // 2 bytes; TODO: Document
);
}
}

View File

@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
date_default_timezone_set('UTC');
require __DIR__ . '/../vendor/autoload.php';