Skip to content

Commit

Permalink
add tar encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
Baptouuuu committed Oct 28, 2023
1 parent c72270f commit 446e362
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"require": {
"php": "~8.2",
"innmind/immutable": "~5.1",
"innmind/filesystem": "~7.1"
"innmind/filesystem": "~7.1",
"innmind/time-continuum": "~3.4"
},
"autoload": {
"psr-4": {
Expand Down
125 changes: 125 additions & 0 deletions proofs/tar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
declare(strict_types = 1);

use Innmind\Encoding\Tar;
use Innmind\Filesystem\{
Adapter\Filesystem,
Name,
Directory,
};
use Innmind\TimeContinuum\Earth;
use Innmind\Url\Path;
use Innmind\Immutable\Predicate\Instance;
use Innmind\BlackBox\Set;

return static function() {
yield proof(
'Tar encoding a single file',
given(Set\Elements::of('amqp.pdf', 'symfony.log')),
static function($assert, $name) {
$clock = new Earth\Clock;
$path = \sys_get_temp_dir().'innmind/encoding/';
$tmp = Filesystem::mount(Path::of($path));
$adapter = Filesystem::mount(Path::of('fixtures/'));
$tar = $adapter
->get(Name::of($name))
->map(static fn($file) => $file->rename(Name::of('other-'.$name)))
->map(Tar::encode($clock))
->match(
static fn($file) => $file,
static fn() => null,
);

$assert
->string($tar->name()->toString())
->startsWith('other-')
->contains($name)
->endsWith('.tar');
$assert->same('application/x-tar', $tar->mediaType()->toString());

$tmp->add($tar);

$exitCode = null;
\exec("tar -xf $path/other-$name.tar --directory=$path", result_code: $exitCode);
$assert->same(0, $exitCode);

$assert->same(
$adapter
->get(Name::of($name))
->match(
static fn($file) => $file->content()->toString(),
static fn() => null,
),
$tmp
->get(Name::of('other-'.$name))
->match(
static fn($file) => $file->content()->toString(),
static fn() => null,
),
);
},
);

yield test(
'Tar encoding a directory',
static function($assert) {
$clock = new Earth\Clock;
$path = \sys_get_temp_dir().'innmind/encoding/';
$tmp = Filesystem::mount(Path::of($path));
$adapter = Filesystem::mount(Path::of('./'));
$tar = $adapter
->get(Name::of('fixtures'))
->map(Tar::encode($clock))
->match(
static fn($file) => $file,
static fn() => null,
);

$assert->same('fixtures.tar', $tar->name()->toString());
$assert->same('application/x-tar', $tar->mediaType()->toString());

$tmp->add($tar);

$exitCode = null;
\exec("tar -xf $path/fixtures.tar --directory=$path", result_code: $exitCode);
$assert->same(0, $exitCode);

$assert->same(
$adapter
->get(Name::of('fixtures'))
->keep(Instance::of(Directory::class))
->flatMap(static fn($fixtures) => $fixtures->get(Name::of('amqp.pdf')))
->match(
static fn($file) => $file->content()->toString(),
static fn() => null,
),
$tmp
->get(Name::of('fixtures'))
->keep(Instance::of(Directory::class))
->flatMap(static fn($fixtures) => $fixtures->get(Name::of('amqp.pdf')))
->match(
static fn($file) => $file->content()->toString(),
static fn() => null,
),
);
$assert->same(
$adapter
->get(Name::of('fixtures'))
->keep(Instance::of(Directory::class))
->flatMap(static fn($fixtures) => $fixtures->get(Name::of('symfony.log')))
->match(
static fn($file) => $file->content()->toString(),
static fn() => null,
),
$tmp
->get(Name::of('fixtures'))
->keep(Instance::of(Directory::class))
->flatMap(static fn($fixtures) => $fixtures->get(Name::of('symfony.log')))
->match(
static fn($file) => $file->content()->toString(),
static fn() => null,
),
);
},
);
};
21 changes: 21 additions & 0 deletions src/Tar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding;

use Innmind\TimeContinuum\Clock;

final class Tar
{
private function __construct()
{
}

/**
* @psalm-pure
*/
public static function encode(Clock $clock): Tar\Encode
{
return Tar\Encode::of($clock);
}
}
189 changes: 189 additions & 0 deletions src/Tar/Encode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php
declare(strict_types = 1);

namespace Innmind\Encoding\Tar;

use Innmind\Encoding\Exception\RuntimeException;
use Innmind\Filesystem\{
File,
File\Content,
Directory,
};
use Innmind\MediaType\MediaType;
use Innmind\TimeContinuum\Clock;
use Innmind\Immutable\{
Str,
Sequence,
};

/**
* @see https://packagist.org/packages/pear/archive_tar This class has been reversed engineered from the pear package
*/
final class Encode
{
private Clock $clock;

/**
* @psalm-mutation-free
*/
private function __construct(Clock $clock)
{
$this->clock = $clock;
}

public function __invoke(File|Directory $file): File
{
return File::named(
$file->name()->toString().'.tar',
Content::ofChunks(
$this
->encode(
$file->name()->str()->toEncoding(Str\Encoding::ascii),
$file,
)
->add(Str::of(
\pack('a1024', ''),
Str\Encoding::ascii,
)),
),
MediaType::of('application/x-tar'),
);
}

/**
* @psalm-pure
*/
public static function of(Clock $clock): self
{
return new self($clock);
}

/**
* @return Sequence<Str>
*/
private function encode(Str $path, File|Directory $file): Sequence
{
if ($path->length() > 99) {
// todo implement @LongLink trick
throw new RuntimeException("'{$path->toString()}' is too long");
}

return match (true) {
$file instanceof File => $this->encodeFile($path, $file),
$file instanceof Directory => $this->encodeDirectory($path, $file),
};
}

/**
* @return Sequence<Str>

Check failure on line 78 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, lowest)

MixedReturnTypeCoercion

src/Tar/Encode.php:78:16: MixedReturnTypeCoercion: The declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeFile is more specific than the inferred return type 'Innmind\Immutable\Sequence<mixed>' (see https://psalm.dev/197)

Check failure on line 78 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, highest)

MixedReturnTypeCoercion

src/Tar/Encode.php:78:16: MixedReturnTypeCoercion: The declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeFile is more specific than the inferred return type 'Innmind\Immutable\Sequence<mixed>' (see https://psalm.dev/197)
*/
private function encodeFile(Str $path, File $file): Sequence
{
$size = $file->content()->size()->match(
static fn($size) => $size->toInt(),
static fn() => $file
->content()
->chunks()
->map(static fn($chunk) => $chunk->toEncoding(Str\Encoding::ascii))
->map(static fn($chunk) => $chunk->length())
->reduce(
0,
static fn(int $sum, int $length) => $sum + $length,
),
);
$header = $this->header($path, $size, File::class);

return $header->append(

Check failure on line 96 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, lowest)

MixedReturnTypeCoercion

src/Tar/Encode.php:96:16: MixedReturnTypeCoercion: The type 'Innmind\Immutable\Sequence<mixed>' is more general than the declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeFile (see https://psalm.dev/197)

Check failure on line 96 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, highest)

MixedReturnTypeCoercion

src/Tar/Encode.php:96:16: MixedReturnTypeCoercion: The type 'Innmind\Immutable\Sequence<mixed>' is more general than the declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeFile (see https://psalm.dev/197)
$file
->content()
->chunks()
->map(static fn($chunk) => $chunk->toEncoding(Str\Encoding::ascii))
->aggregate(static fn(Str $a, Str $b) => $a->append($b)->chunk(512))
->map(static fn($chunk) => \pack('a512', $chunk->toString()))
->map(static fn($chunk) => Str::of($chunk, Str\Encoding::ascii)),
);
}

/**
* @return Sequence<Str>

Check failure on line 108 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, lowest)

MixedReturnTypeCoercion

src/Tar/Encode.php:108:16: MixedReturnTypeCoercion: The declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeDirectory is more specific than the inferred return type 'Innmind\Immutable\Sequence<mixed>' (see https://psalm.dev/197)

Check failure on line 108 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, highest)

MixedReturnTypeCoercion

src/Tar/Encode.php:108:16: MixedReturnTypeCoercion: The declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeDirectory is more specific than the inferred return type 'Innmind\Immutable\Sequence<mixed>' (see https://psalm.dev/197)
*/
private function encodeDirectory(Str $parent, Directory $directory): Sequence
{
$header = $this->header($parent, 0, Directory::class);

$files = $directory
->all()
->flatMap(fn($file) => $this->encode(
$parent->append('/')->append($file->name()->str()),
$file,
));

return $header->append($files);

Check failure on line 121 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, lowest)

MixedReturnTypeCoercion

src/Tar/Encode.php:121:16: MixedReturnTypeCoercion: The type 'Innmind\Immutable\Sequence<mixed>' is more general than the declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeDirectory (see https://psalm.dev/197)

Check failure on line 121 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, highest)

MixedReturnTypeCoercion

src/Tar/Encode.php:121:16: MixedReturnTypeCoercion: The type 'Innmind\Immutable\Sequence<mixed>' is more general than the declared return type 'Innmind\Immutable\Sequence<Innmind\Immutable\Str>' for Innmind\Encoding\Tar\Encode::encodeDirectory (see https://psalm.dev/197)
}

/**
* @param class-string(File)|class-string(Directory) $type

Check failure on line 125 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, lowest)

InvalidDocblock

src/Tar/Encode.php:125:15: InvalidDocblock: Parenthesis must be preceded by “Closure”, “callable”, "pure-callable" or a valid @method name in docblock for Innmind\Encoding\Tar\Encode::header (see https://psalm.dev/008)

Check failure on line 125 in src/Tar/Encode.php

View workflow job for this annotation

GitHub Actions / Psalm (8.2, highest)

InvalidDocblock

src/Tar/Encode.php:125:15: InvalidDocblock: Parenthesis must be preceded by “Closure”, “callable”, "pure-callable" or a valid @method name in docblock for Innmind\Encoding\Tar\Encode::header (see https://psalm.dev/008)
*/
private function header(
Str $path,
int $size,
string $type,
): Sequence {
$fileMode = match ($type) {
File::class => 000644,
Directory::class => 000755,
};
$headerFirstPart = Str::of(
\pack(
'a100a8a8a8a12a12',
$path->toString(), // file name
\sprintf('%07s', \decoct($fileMode & 000777)), // file mode
\sprintf('%07s', \decoct(0)), // user id
\sprintf('%07s', \decoct(0)), // group id
\sprintf('%011s', \decoct($size)), // file size
\sprintf('%011s', \decoct($this->clock->now()->milliseconds())), // file last modification time
),
Str\Encoding::ascii,
);
$headerLastPart = Str::of(
\pack(
'a1a100a6a2a32a32a8a8a155a12',
match ($type) {
File::class => '0',
Directory::class => '5',
}, // link indicator
'', // name of linked file
'ustar ', // format
' ', // format version
'', // owner user name
'', // owner group name
'', // device major number
'', // device minor number
'', // filename prefix
'', // don't know what this is
),
Str\Encoding::ascii,
);
$checksum = $headerFirstPart
->chunk()
->map(static fn($char) => $char->toString())
->append(Sequence::strings()->pad(8, ' ')) // checksum placeholder
->append(
$headerLastPart
->chunk()
->map(static fn($char) => $char->toString()),
)
->map(\ord(...))
->reduce(
0,
static fn(int $sum, int $ord) => $sum + $ord,
);

$packedChecksum = Str::of(
\pack('a8', \sprintf("%06s\0 ", \decoct($checksum))),
Str\Encoding::ascii,
);

return Sequence::lazyStartingWith($headerFirstPart, $packedChecksum, $headerLastPart);
}
}

0 comments on commit 446e362

Please sign in to comment.