Skip to content

Commit

Permalink
add gzip (de)compression
Browse files Browse the repository at this point in the history
  • Loading branch information
Baptouuuu committed Oct 28, 2023
1 parent 9cc0954 commit 8182c37
Show file tree
Hide file tree
Showing 14 changed files with 5,506 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

return Innmind\CodingStandard\CodingStandard::config([
'proofs',
'src',
]);
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"issues": "http://github.com/innmind/encoding/issues"
},
"require": {
"php": "~8.2"
"php": "~8.2",
"innmind/immutable": "~5.1",
"innmind/filesystem": "~7.1"
},
"autoload": {
"psr-4": {
Expand Down
Binary file added fixtures/amqp.pdf
Binary file not shown.
5,000 changes: 5,000 additions & 0 deletions fixtures/symfony.log

Large diffs are not rendered by default.

106 changes: 106 additions & 0 deletions proofs/gzip.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php
declare(strict_types = 1);

use Innmind\Encoding\Gzip;
use Innmind\Filesystem\File\Content;
use Innmind\Immutable\{
Monoid\Concat,
Str\Encoding,
};
use Innmind\BlackBox\Set;

return static function() {
yield proof(
'Gzip compression reduce content size',
given(
Set\Elements::of('fixtures/symfony.log', 'fixtures/amqp.pdf')
->map(\file_get_contents(...)),
),
static function($assert, $file) {
$content = Content::ofString($file);
$compress = Gzip::compress();

$compressed = $compress($content);

$assert
->number($content->size()->match(
static fn($size) => $size->toInt(),
static fn() => null,
))
->greaterThan($compressed->size()->match(
static fn($size) => $size->toInt(),
static fn() => null,
));
},
);

yield proof(
'Gzip compression reduce chunks size',
given(
Set\Elements::of('fixtures/symfony.log', 'fixtures/amqp.pdf')
->map(\file_get_contents(...)),
),
static function($assert, $file) {
$content = Content::ofString($file)->chunks();
$compress = Gzip::compress();

$compressed = $compress($content);

$assert
->number(
$content
->fold(new Concat)
->toEncoding(Encoding::ascii)
->length(),
)
->greaterThan(
$compressed
->fold(new Concat)
->toEncoding(Encoding::ascii)
->length(),
);
},
);

yield proof(
'Gzip compress/decompress returns the original content',
given(Set\Either::any(
Set\Elements::of('fixtures/symfony.log', 'fixtures/amqp.pdf')
->map(\file_get_contents(...)),
Set\Strings::madeOf(Set\Unicode::any())->between(0, 2048),
)),
static function($assert, $file) {
$original = Content::ofString($file);
$compress = Gzip::compress();
$decompress = Gzip::decompress();

$content = $decompress($compress($original));

$assert->same(
$original->toString(),
$content->toString(),
);
},
);

yield proof(
'Gzip compress/decompress returns the original chunks',
given(Set\Either::any(
Set\Elements::of('fixtures/symfony.log', 'fixtures/amqp.pdf')
->map(\file_get_contents(...)),
Set\Strings::madeOf(Set\Unicode::any())->between(0, 2048),
)),
static function($assert, $file) {
$original = Content::ofString($file)->chunks();
$compress = Gzip::compress();
$decompress = Gzip::decompress();

$content = $decompress($compress($original));

$assert->same(
$original->fold(new Concat)->toString(),
$content->fold(new Concat)->toString(),
);
},
);
};
8 changes: 8 additions & 0 deletions src/Exception/Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding\Exception;

interface Exception extends \Throwable
{
}
8 changes: 8 additions & 0 deletions src/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding\Exception;

class RuntimeException extends \RuntimeException implements Exception
{
}
27 changes: 27 additions & 0 deletions src/Gzip.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding;

final class Gzip
{
private function __construct()
{
}

/**
* @psalm-pure
*/
public static function compress(): Gzip\Compress
{
return Gzip\Compress::max();
}

/**
* @psalm-pure
*/
public static function decompress(): Gzip\Decompress
{
return Gzip\Decompress::max();
}
}
75 changes: 75 additions & 0 deletions src/Gzip/Compress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding\Gzip;

use Innmind\Encoding\Gzip\Compress\{
Context,
Chunk,
};
use Innmind\Filesystem\File\Content;
use Innmind\Immutable\{
Sequence,
Str,
};

/**
* @psalm-immutable
*/
final class Compress
{
private function __construct()
{
}

/**
* @template T of Content|Sequence<Str>
*
* @param T $content
*
* @return T
*/
public function __invoke(Content|Sequence $content): Content|Sequence
{
/**
* @psalm-suppress PossiblyInvalidArgument For some reason it doesn't understand the Sequence check
* @var T
*/
return match (true) {
$content instanceof Content => $this->compressContent($content),
$content instanceof Sequence => $this->compressChunks($content),
};
}

/**
* @psalm-pure
*/
public static function max(): self
{
return new self;
}

private function compressContent(Content $content): Content
{
return Content::ofChunks($this->compressChunks($content->chunks()));
}

/**
* @param Sequence<Str> $chunks
*
* @return Sequence<Str>
*/
private function compressChunks(Sequence $chunks): Sequence
{
return Sequence::lazy(static function() use ($chunks) {
// wrapping this context inside a lazy Sequence allows to restart
// the context everytime the sequence is unwrapped
$context = Context::new();

yield $chunks
->map(Chunk::data(...))
->add(Chunk::finish())
->map(static fn($chunk) => $chunk($context));
})->flatMap(static fn($chunks) => $chunks);
}
}
46 changes: 46 additions & 0 deletions src/Gzip/Compress/Chunk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding\Gzip\Compress;

use Innmind\Immutable\Str;

/**
* @internal
*/
final class Chunk
{
private ?Str $data;

/**
* @psalm-mutation-free
*/
private function __construct(?Str $data)
{
$this->data = $data;
}

public function __invoke(Context $context): Str
{
return match ($this->data) {
null => $context->finish(),
default => $context->compress($this->data),
};
}

/**
* @psalm-pure
*/
public static function data(Str $data): self
{
return new self($data);
}

/**
* @psalm-pure
*/
public static function finish(): self
{
return new self(null);
}
}
53 changes: 53 additions & 0 deletions src/Gzip/Compress/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding\Gzip\Compress;

use Innmind\Encoding\Exception\RuntimeException;
use Innmind\Immutable\Str;

/**
* @internal
*/
final class Context
{
private \DeflateContext $context;

private function __construct()
{
$this->context = \deflate_init(
\ZLIB_ENCODING_GZIP,
['level' => 9],
);
}

public static function new(): self
{
return new self;
}

public function compress(Str $data): Str
{
return $data->map(function($string) use ($data) {
$compressed = \deflate_add(
$this->context,
$string,
\ZLIB_NO_FLUSH,
);

return match ($compressed) {
false => throw new RuntimeException('Failed to compress data'),
default => $compressed,
};
});
}

public function finish(): Str
{
return Str::of(\deflate_add(
$this->context,
'',
\ZLIB_FINISH,
));
}
}

0 comments on commit 8182c37

Please sign in to comment.