Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8d30ce8
Implement snappy compression in userland code
thekid Aug 2, 2025
44c009f
Fix PHP < 7.4 by using long closures
thekid Aug 2, 2025
39f73c5
Implement reading snappy compressed data from a stream
thekid Aug 2, 2025
72f4860
Add unittests for SnappyInputStream
thekid Aug 2, 2025
536b6c5
Extend test suite with erroneous snappy compressed data
thekid Aug 2, 2025
371d9d5
Remove unreachable code
thekid Aug 2, 2025
90fef34
Free all previous unreferenced blocks
thekid Aug 3, 2025
85c25ab
Simplify reading literal lengths
thekid Aug 3, 2025
fa5ad1a
Unify error messages
thekid Aug 3, 2025
53f42ff
Extract encoding length as helper
thekid Aug 3, 2025
ca630ab
Merge branch 'main' into feature/snappy-compression
thekid Aug 3, 2025
e2fac75
Adjust compress() and create() signatures
thekid Aug 3, 2025
ff4614f
Extract buffered compression into its dedicated class
thekid Aug 3, 2025
b8bb093
Add output stream
thekid Aug 3, 2025
c4eb43d
Remove defaults
thekid Aug 3, 2025
e8a7b7f
MFH: Drop PHP < 7.4. This enables us to use unpack() with offsets
thekid Aug 3, 2025
d44d87a
Mention Snappy in README file [skip ci]
thekid Aug 3, 2025
b667e27
QA: Use short closures where applicable
thekid Aug 3, 2025
91c21a4
Add compression test for repeated input
thekid Aug 3, 2025
d6907ff
Fix decompressing literals
thekid Aug 3, 2025
6474a4e
Fix fragment encoding in compress()
thekid Aug 3, 2025
6e66f00
Fix "Argument 3 ($offset) must be contained in argument 2 ($data)"
thekid Aug 3, 2025
8085296
QA: Remove emit initialization
thekid Aug 3, 2025
bb159bb
Fix fragment encoding when streaming
thekid Aug 3, 2025
084171d
Fix repeated_input_compressed() test
thekid Aug 3, 2025
ded3712
Remove inline helper
thekid Aug 3, 2025
d2fe0bf
Remove start which is always 0
thekid Aug 3, 2025
2ae0b98
QA: Extract encoding methods to Snappy class
thekid Aug 3, 2025
03d23c6
Ensure streams are closed
thekid Aug 3, 2025
fff87b0
Close underlying stream in BufferedOutputStream
thekid Aug 3, 2025
f6cd74b
Refrain from initializing length twice
thekid Aug 3, 2025
e95440f
Add snappy to Compression::algorithms() output [skip ci]
thekid Aug 3, 2025
8251a87
Add buffered input to complement buffered output
thekid Aug 5, 2025
d0468c6
Support literal lengths encoded with 1 to 4 extra bytes (not just 2)
thekid Aug 7, 2025
8e4ec2a
Implement COPY_4 in compressor
thekid Aug 7, 2025
310ad3d
MFH
thekid Aug 15, 2025
1c46b03
Add quote about snappy from Wikipedia [skip ci]
thekid Aug 15, 2025
9b812e7
Be consistent about ordering of algorithms [skip ci]
thekid Aug 15, 2025
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Compression streams
[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/)
[![Latest Stable Version](https://poser.pugx.org/xp-forge/compression/version.svg)](https://packagist.org/packages/xp-forge/compression)

Compressing output and decompressing input streams including GZip, BZip2, ZStandard and Brotli.
Compressing output and decompressing input streams including GZip, BZip2, Brotli, Snappy and ZStandard.

Examples
--------
Expand Down Expand Up @@ -39,11 +39,12 @@ $out->close();

Dependencies
------------
Compression algorithms are implemented in C and thus require a specific PHP extension:
Compression algorithms might require a specific PHP extension:

* **GZip** - requires PHP's ["zlib" extension](https://www.php.net/zlib)
* **Bzip2** - requires PHP's ["bzip2" extension](https://www.php.net/bzip2)
* **Brotli** - requires https://github.com/kjdev/php-ext-brotli
* **Snappy** - *no dependencies, implemented in userland*
* **ZStandard** - requires https://github.com/kjdev/php-ext-zstd

Accessing these algorithms can be done via the `Compression` API:
Expand Down Expand Up @@ -97,6 +98,7 @@ io.streams.compress.Algorithms@{
io.streams.compress.Gzip(token: gzip, extension: .gz, supported: true, levels: 1..9)
io.streams.compress.Bzip2(token: bzip2, extension: .bz2, supported: false, levels: 1..9)
io.streams.compress.Brotli(token: br, extension: .br, supported: true, levels: 1..11)
io.streams.compress.Snappy(token: snappy, extension: .sn, supported: true, levels: 0..0)
io.streams.compress.ZStandard(token: zstd, extension: .zstd, supported: true, levels: 1..22)
}
```
Expand Down Expand Up @@ -137,4 +139,5 @@ $in->close();

See also
--------
* The PHP RFC [Modern Compression](https://wiki.php.net/rfc/modern_compression) suggests adding *zstd* and *brotli* into PHP.
* The PHP RFC [Modern Compression](https://wiki.php.net/rfc/modern_compression) suggests adding *zstd* and *brotli* into PHP.
* Snappy *does not aim for maximum compression, or compatibility with any other compression library; instead, it aims for very high speeds and reasonable compression*, quoting [its Wikipedia page](https://en.wikipedia.org/wiki/Snappy_(compression))
10 changes: 8 additions & 2 deletions src/main/php/io/streams/Compression.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace io\streams;

use io\streams\compress\{Algorithm, Algorithms, None, Brotli, Bzip2, Gzip, ZStandard};
use io\streams\compress\{Algorithm, Algorithms, None, Brotli, Bzip2, Gzip, Snappy, ZStandard};
use lang\MethodNotImplementedException;

/**
Expand All @@ -22,7 +22,13 @@ static function __static() {
self::$NONE= new None();

// Register known algorithms included in this library
self::$algorithms= (new Algorithms())->add(new Gzip(), new Bzip2(), new Brotli(), new ZStandard());
self::$algorithms= (new Algorithms())->add(
new Gzip(),
new Bzip2(),
new Brotli(),
new Snappy(),
new ZStandard()
);
}

/**
Expand Down
78 changes: 78 additions & 0 deletions src/main/php/io/streams/compress/BufferedInputStream.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php namespace io\streams\compress;

use io\streams\InputStream;
use lang\IllegalArgumentException;

/** @test io.streams.compress.unittest.BufferedInputStreamTest */
class BufferedInputStream implements InputStream {
private $in, $decompress;
private $buffer= null, $position= 0;

/**
* Creates a new decompressing input stream
*
* @param io.streams.InputStream $in The stream to read from
* @param io.streams.compress.Algorithm|function(string): string $decompress
* @throws lang.IllegalArgumentException
*/
public function __construct(InputStream $in, $decompress) {
if ($decompress instanceof Algorithm) {
$this->decompress= [$decompress, 'decompress'];
} else if (is_callable($decompress)) {
$this->decompress= $decompress;
} else {
throw new IllegalArgumentException('Expected an Algorithm or a callable, have '.typeof($decompress));
}
$this->in= $in;
}

/** @return string */
private function buffer() {
if (null === $this->buffer) {
$compressed= '';
while ($this->in->available()) {
$compressed.= $this->in->read();
}

$this->buffer= ($this->decompress)($compressed);
}
return $this->buffer;
}

/**
* Read a string
*
* @param int limit default 8192
* @return string
*/
public function read($limit= 8192) {
$chunk= substr($this->buffer(), $this->position, $limit);
$this->position+= strlen($chunk);
return $chunk;
}

/**
* Returns the number of bytes that can be read from this stream
* without blocking.
*
* @return int
*/
public function available() {
return strlen($this->buffer()) - $this->position;
}

/**
* Close this buffer.
*
* @return void
*/
public function close() {
$this->buffer= null;
$this->in->close();
}

/** Ensures input stream is closed */
public function __destruct() {
$this->close();
}
}
67 changes: 67 additions & 0 deletions src/main/php/io/streams/compress/BufferedOutputStream.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php namespace io\streams\compress;

use io\streams\OutputStream;
use lang\IllegalArgumentException;

/** @test io.streams.compress.unittest.BufferedOutputStreamTest */
class BufferedOutputStream implements OutputStream {
private $out, $compress;
private $buffer= '';

/**
* Creates a new compressing output stream
*
* @param io.streams.OutputStream $out The stream to write to
* @param io.streams.compress.Algorithm|function(string): string $compress
* @throws lang.IllegalArgumentException
*/
public function __construct(OutputStream $out, $compress) {
if ($compress instanceof Algorithm) {
$this->compress= [$compress, 'compress'];
} else if (is_callable($compress)) {
$this->compress= $compress;
} else {
throw new IllegalArgumentException('Expected an Algorithm or a callable, have '.typeof($compress));
}
$this->out= $out;
}

/**
* Write a string
*
* @param var $arg
* @return void
*/
public function write($arg) {
$this->buffer.= $arg;
}

/**
* Flush this buffer
*
* @return void
*/
public function flush() {
// NOOP
}

/**
* Closes this object. May be called more than once, which may
* not fail - that is, if the object is already closed, this
* method should have no effect.
*
* @return void
*/
public function close() {
if (null !== $this->buffer) {
$this->out->write(($this->compress)($this->buffer));
$this->buffer= null;
}
$this->out->close();
}

/** Ensures output stream is closed */
public function __destruct() {
$this->close();
}
}
Loading