-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
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, | ||
), | ||
); | ||
}, | ||
); | ||
}; |
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); | ||
} | ||
} |
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 GitHub Actions / Psalm (8.2, lowest)MixedReturnTypeCoercion
Check failure on line 78 in src/Tar/Encode.php GitHub Actions / Psalm (8.2, highest)MixedReturnTypeCoercion
|
||
*/ | ||
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 GitHub Actions / Psalm (8.2, lowest)MixedReturnTypeCoercion
Check failure on line 96 in src/Tar/Encode.php GitHub Actions / Psalm (8.2, highest)MixedReturnTypeCoercion
|
||
$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 GitHub Actions / Psalm (8.2, lowest)MixedReturnTypeCoercion
Check failure on line 108 in src/Tar/Encode.php GitHub Actions / Psalm (8.2, highest)MixedReturnTypeCoercion
|
||
*/ | ||
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 GitHub Actions / Psalm (8.2, lowest)MixedReturnTypeCoercion
Check failure on line 121 in src/Tar/Encode.php GitHub Actions / Psalm (8.2, highest)MixedReturnTypeCoercion
|
||
} | ||
|
||
/** | ||
* @param class-string(File)|class-string(Directory) $type | ||
Check failure on line 125 in src/Tar/Encode.php GitHub Actions / Psalm (8.2, lowest)InvalidDocblock
Check failure on line 125 in src/Tar/Encode.php GitHub Actions / Psalm (8.2, highest)InvalidDocblock
|
||
*/ | ||
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); | ||
} | ||
} |