Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/prado.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
with:
php-version: ${{ matrix.php-versions }}
extensions: ctype, dom, intl, json, mbstring, memcached, pdo_mysql, pdo_pgsql, pdo_sqlite, openssl, pcre, spl, zlib
extensions: ctype, dom, intl, json, mbstring, memcached, pdo_mysql, pdo_pgsql, pdo_sqlite, openssl, pcre, spl, zlib, bz2, zstd, brotli
tools: php-cs-fixer, phpstan, cs2pr

- name: Validate composer.json and composer.lock
Expand Down Expand Up @@ -174,7 +174,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: ctype, dom, intl, json, mbstring, memcached, pdo_mysql, pdo_pgsql, pdo_sqlite, openssl, pcre, spl, zlib
extensions: ctype, dom, intl, json, mbstring, memcached, pdo_mysql, pdo_pgsql, pdo_sqlite, openssl, pcre, spl, zlib, bz2, zstd, brotli
tools: php-cs-fixer, phpstan, cs2pr

- name: Validate composer.json and composer.lock
Expand Down Expand Up @@ -243,7 +243,7 @@ jobs:
with:
php-version: ${{ env.PHP_VERSION }}
# memcached extension omitted: libmemcached is not available on Windows
extensions: ctype, dom, intl, json, mbstring, pdo_mysql, pdo_pgsql, pdo_sqlite, openssl, pcre, spl, bz2, xsl, zip, zlib
extensions: ctype, dom, intl, json, mbstring, pdo_mysql, pdo_pgsql, pdo_sqlite, openssl, pcre, spl, bz2, xsl, zip, zlib, zstd, brotli
tools: php-cs-fixer, phpstan, cs2pr

- name: Validate composer.json and composer.lock
Expand Down
9 changes: 9 additions & 0 deletions framework/Exceptions/messages/messages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,15 @@ streamwrapper_already_registered = Stream wrapper protocol '{0}' is already reg
streamwrapper_registration_failed = Unable to register stream wrapper protocol '{0}' for class '{1}'.
reservedspace_invalid = A reserved space requires a non-negative offset and a positive length; offset '{0}' and length '{1}' were given.
reservedspace_access_denied = The operation overlaps the reserved space at offset '{0}'.
builtincompressor_extension_required = The '{0}' compressor requires the '{1}' PHP extension.
builtincompressor_compress_failed = The '{0}' compressor failed to compress the data.
builtincompressor_decompress_failed = The '{0}' compressor failed to decompress the data; it may be corrupt or in another format.
compression_method_unknown = The compression method '{0}' is unknown or its extension is not available.
compression_none_available = No compression method is available in this PHP installation.
compression_xz_unsupported = xz/LZMA has no PHP extension; install belisoful/prado-compression for a native-or-CLI xz codec.
clicompressor_command_missing = The '{0}' compressor requires one of these system commands on the PATH: '{1}'.
clicompressor_process_failed = The '{0}' compressor could not start the '{1}' command.
clicompressor_command_failed = The '{0}' compressor command '{1}' exited with code {2}: {3}

socketservermodule_endpoint_required = TSocketServerModule requires an Endpoint, or a positive Port to compose one.
socketservermodule_serverclass_invalid = TSocketServerModule ServerClass '{0}' is invalid. It must be TSocketServer or a subclass.
Expand Down
44 changes: 44 additions & 0 deletions framework/IO/Compression/ICompressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* ICompressor interface file.
*
* @author Brad Anderson <belisoful@icloud.com>
* @link https://github.com/pradosoft/prado
* @license https://github.com/pradosoft/prado/blob/master/LICENSE
*/

namespace Prado\IO\Compression;

/**
* ICompressor interface.
*
* A whole-string compression codec: {@see compress()} encodes a byte string and
* {@see decompress()} restores it. Implementing it lets a caller select a compression
* codec without binding to one algorithm, whether hand-written (LZW, run-length) or an
* extension-gated wrapper (zstd, brotli). An implementation may accept additional
* optional parameters (a compression level, a format-specific setting) after the data.
*
* A codec that transforms incrementally, without holding the whole string, extends
* {@see \Prado\IO\Filter\TStreamCodecFilter} instead; the two are companion forms of the
* same algorithm, one whole-string and one streaming.
*
* @author Brad Anderson <belisoful@icloud.com>
* @since 4.4.0
*/
interface ICompressor
{
/**
* Compresses a byte string.
* @param string $data The raw bytes.
* @return string The encoded bytes.
*/
public static function compress(string $data): string;

/**
* Decompresses a byte string produced by {@see compress()}.
* @param string $data The encoded bytes.
* @return string The decoded bytes.
*/
public static function decompress(string $data): string;
}
66 changes: 66 additions & 0 deletions framework/IO/Compression/TBrotliCompressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/**
* TBrotliCompressor class file.
*
* @author Brad Anderson <belisoful@icloud.com>
* @link https://github.com/pradosoft/prado
* @license https://github.com/pradosoft/prado/blob/master/LICENSE
*/

namespace Prado\IO\Compression;

/**
* TBrotliCompressor class.
*
* Wraps the Brotli functions for the `br` format: a codec that compresses text more tightly
* than gzip, which is why browsers advertise it for web content. It is the HTTP
* `Content-Encoding: br` coding. The functions live in the optional `brotli` extension, so
* {@see isAvailable()} reports whether the codec can run before it is used; the framework
* never bundles a fallback implementation.
*
* The quality runs 0..11 (higher is smaller and slower); -1 selects {@see DEFAULT_QUALITY},
* the maximum, which is Brotli's own default.
*
* @author Brad Anderson <belisoful@icloud.com>
* @since 4.4.0
*/
class TBrotliCompressor extends TBuiltinCompressor
{
/** The wire-format name (the HTTP content-coding token). */
public const NAME = 'br';

/** The backing PHP extension. */
protected const EXTENSION = 'brotli';

/** The lowest Brotli quality. */
public const MIN_QUALITY = 0;

/** The highest Brotli quality, which is also its default. */
public const MAX_QUALITY = 11;

/** The Brotli quality used when no explicit level is given. */
public const DEFAULT_QUALITY = 11;

/**
* Compresses with brotli_compress.
* @param string $data The raw bytes.
* @param int $level The quality 0..11, or -1 for {@see DEFAULT_QUALITY}.
* @return false|string The brotli bytes, or false on failure.
*/
protected static function encode(string $data, int $level): string|false
{
$quality = ($level < self::MIN_QUALITY || $level > self::MAX_QUALITY) ? self::DEFAULT_QUALITY : $level;
return @brotli_compress($data, $quality);
}

/**
* Decompresses with brotli_uncompress.
* @param string $data The brotli bytes.
* @return false|string The raw bytes, or false on failure.
*/
protected static function decode(string $data): string|false
{
return @brotli_uncompress($data);
}
}
107 changes: 107 additions & 0 deletions framework/IO/Compression/TBuiltinCompressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

/**
* TBuiltinCompressor class file.
*
* @author Brad Anderson <belisoful@icloud.com>
* @link https://github.com/pradosoft/prado
* @license https://github.com/pradosoft/prado/blob/master/LICENSE
*/

namespace Prado\IO\Compression;

use Prado\Exceptions\TIOException;

/**
* TBuiltinCompressor class.
*
* The shared base for the {@see ICompressor} codecs that wrap PHP's native compression
* functions, so the framework exposes the standard formats without reimplementing them.
* A subclass names its required extension through {@see EXTENSION} and provides the two
* native calls through {@see encode()} and {@see decode()}; this base adds the availability
* guard, the compression-level parameter, and uniform {@see TIOException} failure reporting.
*
* A native call reports failure by returning a non-string (`false` for the zlib functions,
* an integer error code for the bzip2 functions), so both surface as a thrown exception
* rather than a silent wrong value. The concrete codecs are {@see TGzipCompressor},
* {@see TZlibCompressor}, {@see TDeflateCompressor}, and {@see TBzip2Compressor}.
*
* @author Brad Anderson <belisoful@icloud.com>
* @since 4.4.0
*/
abstract class TBuiltinCompressor implements ICompressor
{
/** The wire-format name of the codec, for diagnostics and content negotiation. */
public const NAME = '';

/** The PHP extension the codec requires, or '' when it is part of the PHP core. */
protected const EXTENSION = '';

/**
* Returns whether the codec's backing extension is loaded in this PHP installation.
* @return bool Whether the codec can run.
*/
public static function isAvailable(): bool
{
return static::EXTENSION === '' || extension_loaded(static::EXTENSION);
}

/**
* Compresses a byte string with the native codec.
* @param string $data The raw bytes.
* @param int $level The compression level, or -1 for the codec's default.
* @throws TIOException When the backing extension is absent or the native call fails.
* @return string The compressed bytes.
*/
public static function compress(string $data, int $level = -1): string
{
static::assertAvailable();
$result = static::encode($data, $level);
if (!is_string($result)) {
throw new TIOException('builtincompressor_compress_failed', static::NAME);
}
return $result;
}

/**
* Decompresses a byte string produced by {@see compress()}.
* @param string $data The compressed bytes.
* @throws TIOException When the backing extension is absent or the data is corrupt.
* @return string The decompressed bytes.
*/
public static function decompress(string $data): string
{
static::assertAvailable();
$result = static::decode($data);
if (!is_string($result)) {
throw new TIOException('builtincompressor_decompress_failed', static::NAME);
}
return $result;
}

/**
* Asserts the backing extension is loaded before a native call.
* @throws TIOException When the extension is absent.
*/
protected static function assertAvailable(): void
{
if (!static::isAvailable()) {
throw new TIOException('builtincompressor_extension_required', static::NAME, static::EXTENSION);
}
}

/**
* Runs the native compression call.
* @param string $data The raw bytes.
* @param int $level The compression level, or -1 for the codec's default.
* @return false|int|string The compressed bytes, or a non-string on failure.
*/
abstract protected static function encode(string $data, int $level): string|int|false;

/**
* Runs the native decompression call.
* @param string $data The compressed bytes.
* @return false|int|string The decompressed bytes, or a non-string on failure.
*/
abstract protected static function decode(string $data): string|int|false;
}
60 changes: 60 additions & 0 deletions framework/IO/Compression/TBzip2Compressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* TBzip2Compressor class file.
*
* @author Brad Anderson <belisoful@icloud.com>
* @link https://github.com/pradosoft/prado
* @license https://github.com/pradosoft/prado/blob/master/LICENSE
*/

namespace Prado\IO\Compression;

/**
* TBzip2Compressor class.
*
* Wraps PHP's {@see https://www.php.net/bzcompress bzcompress}/{@see https://www.php.net/bzdecompress
* bzdecompress} for the bzip2 format: a Burrows-Wheeler codec that compresses more tightly
* than DEFLATE on many inputs at a higher CPU cost. This is the format of a `.bz2` file.
* The bzip2 functions live in the optional `bz2` extension, so {@see isAvailable()} reports
* whether the codec can run before it is used.
*
* The compression level is a bzip2 block size of 1..9 (each step is 100 KB of working
* memory); -1 selects the library default.
*
* @author Brad Anderson <belisoful@icloud.com>
* @since 4.4.0
*/
class TBzip2Compressor extends TBuiltinCompressor
{
/** The wire-format name. */
public const NAME = 'bzip2';

/** The backing PHP extension. */
protected const EXTENSION = 'bz2';

/** The bzip2 block size used when no explicit level is given. */
public const DEFAULT_BLOCK_SIZE = 4;

/**
* Compresses with bzcompress.
* @param string $data The raw bytes.
* @param int $level The bzip2 block size 1..9, or -1 for {@see DEFAULT_BLOCK_SIZE}.
* @return int|string The bzip2 bytes, or a bzip2 error number on failure.
*/
protected static function encode(string $data, int $level): string|int
{
$blockSize = ($level < 1 || $level > 9) ? self::DEFAULT_BLOCK_SIZE : $level;
return bzcompress($data, $blockSize);
}

/**
* Decompresses with bzdecompress.
* @param string $data The bzip2 bytes.
* @return int|string The raw bytes, or a bzip2 error number on failure.
*/
protected static function decode(string $data): string|int
{
return bzdecompress($data);
}
}
70 changes: 70 additions & 0 deletions framework/IO/Compression/TCliCompressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/**
* TCliCompressor class file.
*
* @author Brad Anderson <belisoful@icloud.com>
* @link https://github.com/pradosoft/prado
* @license https://github.com/pradosoft/prado/blob/master/LICENSE
*/

namespace Prado\IO\Compression;

/**
* TCliCompressor class.
*
* The base for an {@see ICompressor} codec that is backed only by a system command, for a
* format with no PHP extension at all. It wires the {@see TCliCompressorTrait} command
* backend to the {@see ICompressor} contract: {@see isAvailable()} reports whether the
* command is present, and {@see compress()}/{@see decompress()} run it. A subclass supplies
* the command and its arguments through {@see commands()}, {@see compressArgs()}, and
* {@see decompressArgs()}.
*
* A codec whose format also has a PHP extension does not extend this; it extends
* {@see TBuiltinCompressor} and mixes in {@see TCliCompressorTrait} directly to fall back to
* the command only when the extension is absent. A CLI codec spawns a process per call, so
* it costs far more than a native codec and suits an occasional whole-string transform, not a
* hot loop.
*
* @author Brad Anderson <belisoful@icloud.com>
* @since 4.4.0
*/
abstract class TCliCompressor implements ICompressor
{
use TCliCompressorTrait;

/** The wire-format name of the codec. */
public const NAME = '';

/**
* Returns whether the backing command is available on this system.
* @return bool Whether the codec can run.
*/
public static function isAvailable(): bool
{
return static::cliAvailable();
}

/**
* Compresses a byte string by running the command.
* @param string $data The raw bytes.
* @param int $level The compression level, or -1 for the command's default.
* @throws \Prado\Exceptions\TIOException When the command is missing or fails.
* @return string The compressed bytes.
*/
public static function compress(string $data, int $level = -1): string
{
return static::cliCompress($data, $level);
}

/**
* Decompresses a byte string by running the command.
* @param string $data The compressed bytes.
* @throws \Prado\Exceptions\TIOException When the command is missing, the data is corrupt, or the command fails.
* @return string The decompressed bytes.
*/
public static function decompress(string $data): string
{
return static::cliDecompress($data);
}
}
Loading
Loading