<?php
declare(strict_types=1);
/*
* This file is part of the nelexa/zip package.
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PhpZip\IO\Stream;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
/**
* The class provides stream reuse functionality.
*
* Stream will not be closed at {@see fclose}.
*
* @see https://www.php.net/streamwrapper
*/
final class ZipEntryStreamWrapper
{
/** @var string the registered protocol */
public const PROTOCOL = 'zipentry';
/** @var resource */
public $context;
/** @var resource */
private $fp;
public static function register(): bool
{
$protocol = self::PROTOCOL;
if (!\in_array($protocol, stream_get_wrappers(), true)) {
if (!stream_wrapper_register($protocol, self::class)) {
throw new \RuntimeException("Failed to register '{$protocol}://' protocol");
}
return true;
}
return false;
}
public static function unregister(): void
{
stream_wrapper_unregister(self::PROTOCOL);
}
/**
* @return resource
*/
public static function wrap(ZipEntry $entry)
{
self::register();
$context = stream_context_create(
[
self::PROTOCOL => [
'entry' => $entry,
],
]
);
$uri = self::PROTOCOL . '://' . $entry->getName();
$fp = fopen($uri, 'r+b', false, $context);
if ($fp === false) {
throw new \RuntimeException('Error open ' . $uri);
}
return $fp;
}
/**
* Opens file or URL.
*
* This method is called immediately after the wrapper is
* initialized (f.e. by {@see fopen()} and {@see file_get_contents()}).
*
* @param string $path specifies the URL that was passed to
* the original function
* @param string $mode the mode used to open the file, as detailed
* for {@see fopen()}
* @param int $options Holds additional flags set by the streams
* API. It can hold one or more of the
* following values OR'd together.
* @param string|null $opened_path if the path is opened successfully, and
* STREAM_USE_PATH is set in options,
* opened_path should be set to the
* full path of the file/resource that
* was actually opened
*
* @throws ZipException
*
* @see https://www.php.net/streamwrapper.stream-open
*/
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
{
if ($this->context === null) {
throw new \RuntimeException('stream context is null');
}
$streamOptions = stream_context_get_options($this->context);
if (!isset($streamOptions[self::PROTOCOL]['entry'])) {
throw new \RuntimeException('no stream option ["' . self::PROTOCOL . '"]["entry"]');
}
$zipEntry = $streamOptions[self::PROTOCOL]['entry'];
if (!$zipEntry instanceof ZipEntry) {
throw new \RuntimeException('invalid stream context');
}
$zipData = $zipEntry->getData();
if ($zipData === null) {
throw new ZipException(sprintf('No data for zip entry "%s"', $zipEntry->getName()));
}
$this->fp = $zipData->getDataAsStream();
return $this->fp !== false;
}
/**
* Read from stream.
*
* This method is called in response to {@see fread()} and {@see fgets()}.
*
* Note: Remember to update the read/write position of the stream
* (by the number of bytes that were successfully read).
*
* @param int $count how many bytes of data from the current
* position should be returned
*
* @return false|string If there are less than count bytes available,
* return as many as are available. If no more data
* is available, return either FALSE or
* an empty string.
*
* @see https://www.php.net/streamwrapper.stream-read
*/
public function stream_read(int $count)
{
return fread($this->fp, $count);
}
/**
* Seeks to specific location in a stream.
*
* This method is called in response to {@see fseek()}.
* The read/write position of the stream should be updated according
* to the offset and whence.
*
* @param int $offset the stream offset to seek to
* @param int $whence Possible values:
* {@see \SEEK_SET} - Set position equal to offset bytes.
* {@see \SEEK_CUR} - Set position to current location plus offset.
* {@see \SEEK_END} - Set position to end-of-file plus offset.
*
* @return bool return TRUE if the position was updated, FALSE otherwise
*
* @see https://www.php.net/streamwrapper.stream-seek
*/
public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
{
return fseek($this->fp, $offset, $whence) === 0;
}
/**
* Retrieve the current position of a stream.
*
* This method is called in response to {@see fseek()} to determine
* the current position.
*
* @return int should return the current position of the stream
*
* @see https://www.php.net/streamwrapper.stream-tell
*/
public function stream_tell(): int
{
$pos = ftell($this->fp);
if ($pos === false) {
throw new \RuntimeException('Cannot get stream position.');
}
return $pos;
}
/**
* Tests for end-of-file on a file pointer.
*
* This method is called in response to {@see feof()}.
*
* @return bool should return TRUE if the read/write position is at
* the end of the stream and if no more data is available
* to be read, or FALSE otherwise
*
* @see https://www.php.net/streamwrapper.stream-eof
*/
public function stream_eof(): bool
{
return feof($this->fp);
}
/**
* Retrieve information about a file resource.
*
* This method is called in response to {@see fstat()}.
*
* @see https://www.php.net/streamwrapper.stream-stat
* @see https://www.php.net/stat
* @see https://www.php.net/fstat
*/
public function stream_stat(): array
{
return fstat($this->fp);
}
/**
* Flushes the output.
*
* This method is called in response to {@see fflush()} and when the
* stream is being closed while any unflushed data has been written to
* it before.
* If you have cached data in your stream but not yet stored it into
* the underlying storage, you should do so now.
*
* @return bool should return TRUE if the cached data was successfully
* stored (or if there was no data to store), or FALSE
* if the data could not be stored
*
* @see https://www.php.net/streamwrapper.stream-flush
*/
public function stream_flush(): bool
{
return fflush($this->fp);
}
/**
* Truncate stream.
*
* Will respond to truncation, e.g., through {@see ftruncate()}.
*
* @param int $newSize the new size
*
* @return bool returns TRUE on success or FALSE on failure
*
* @see https://www.php.net/streamwrapper.stream-truncate
*/
public function stream_truncate(int $newSize): bool
{
return ftruncate($this->fp, $newSize);
}
/**
* Write to stream.
*
* This method is called in response to {@see fwrite().}
*
* Note: Remember to update the current position of the stream by
* number of bytes that were successfully written.
*
* @param string $data should be stored into the underlying stream
*
* @return int should return the number of bytes that were successfully stored, or 0 if none could be stored
*
* @see https://www.php.net/streamwrapper.stream-write
*/
public function stream_write(string $data): int
{
$bytes = fwrite($this->fp, $data);
return $bytes === false ? 0 : $bytes;
}
/**
* Retrieve the underlaying resource.
*
* This method is called in response to {@see stream_select()}.
*
* @param int $cast_as can be {@see STREAM_CAST_FOR_SELECT} when {@see stream_select()}
* is callingstream_cast() or {@see STREAM_CAST_AS_STREAM} when
* stream_cast() is called for other uses
*
* @return resource
*/
public function stream_cast(int $cast_as)
{
return $this->fp;
}
/**
* Close a resource.
*
* This method is called in response to {@see fclose()}.
* All resources that were locked, or allocated, by the wrapper should be released.
*
* @see https://www.php.net/streamwrapper.stream-close
*/
public function stream_close(): void
{
}
}