Use custom TAR builder instead of PharData

This commit is contained in:
the-djmaze 2026-11-02 00:54:36 +01:00
parent 61a16e2e72
commit 2de4ebac54
2 changed files with 282 additions and 5 deletions

View file

@ -2,6 +2,7 @@
<?php
define('ROOT_DIR', dirname(__DIR__));
chdir(ROOT_DIR);
require(ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/snappymail/tar.php');
$options = getopt('', ['aur','docker','plugins','skip-gulp','debian','nextcloud','owncloud','cpanel','sign']);
@ -98,7 +99,9 @@ if (!$zip->open($zip_destination, ZIPARCHIVE::CREATE)) {
exit("Failed to create {$zip_destination}");
}
$tar = new PharData($tar_destination);
$tar_destination .= '.gz';
$tar = new \SnappyMail\TAR($tar_destination);
//$tar = new PharData($tar_destination);
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
@ -158,10 +161,13 @@ $zip->addFile('README.md');
$tar->addFile('README.md');
$zip->close();
$tar->compress(Phar::GZ);
unlink($tar_destination);
$tar_destination .= '.gz';
if ($tar instanceof PharData) {
$tar->compress(Phar::GZ);
unlink($tar_destination);
$tar_destination .= '.gz';
} else {
$tar->close();
}
echo "{$zip_destination} created\n{$tar_destination} created\n";

View file

@ -98,4 +98,275 @@ class TAR
\gzclose($fp);
return true;
}
const
TYPE_FILE = 0,
TYPE_LINK = 2,
TYPE_DIR = 5;
protected
$gzip = array('w' => null, 'h' => null, 'l' => 0),
$stream = null,
$stream_info = null;
public function start(): void
{
if ($this->stream) {
return;
}
$this->stream = \fopen($this->filename, 'wb');
if (\str_ends_with($this->filename, '.gz')) {
// Write gzip header, see http://www.zlib.org/rfc-gzip.html#member-format
if (!\fwrite($this->stream, "\x1F\x8B\x08\x00".\pack('V', \time())."\0\x03", 10)) {
throw new \Exception('Failed to write to stream');
}
// Start compression
$this->gzip['w'] = \stream_filter_append($this->stream, 'zlib.deflate', STREAM_FILTER_WRITE, array(
'level' => 9,
// 'window' => 32768,
'memory' => 9,
));
// Start CRC32 hashing
$this->gzip['h'] = \hash_init('crc32b');
}
$this->stream_info = new \SplFileInfo($this->filename);
}
function __destruct()
{
$this->close();
}
public function close(): bool
{
if (!$this->stream) {
return false;
}
// Write tar footer
$this->write(\pack('a1024', ''));
// Stop compression
if ($this->gzip['w']) {
\stream_filter_remove($this->gzip['w']);
// hash_final is a string, not an integer
$crc = \hash_final($this->gzip['h'], 1);
// write the little endian CRC32 and uncompressed file size
\fwrite($this->stream, $crc[3].$crc[2].$crc[1].$crc[0].\pack('V', $this->gzip['l']), 8);
}
\fclose($this->stream);
$this->stream = $this->stream_info = null;
return true;
}
public function addFile(string $name, ?string $localName = null, ?\SplFileInfo $fileinfo = null): void
{
$this->start();
if (!$fileinfo) {
$fileinfo = new \SplFileInfo($name);
}
if (!$localName) {
$localName = $name;
}
if ($fileinfo->isLink()) {
$stat = \lstat($fileinfo);
$target = $fileinfo->getLinkTarget();
if (\dirname($fileinfo->getPathname()) === \dirname($fileinfo->getRealPath())) {
$target = \basename($target);
}
$this->writeEntryHeader(
$localName,
static::TYPE_LINK,
0,
$stat['mode'],
$target
);
} else if ($fileinfo->isDir()) {
$this->writeEntryHeader(
\rtrim($localName, '/') . '/',
static::TYPE_DIR,
0,
$fileinfo->getPerms()
);
} else {
$file = $fileinfo->openFile('rb');
$this->writeEntryHeader(
$localName,
static::TYPE_FILE,
$file->getSize(),
$fileinfo->getPerms()
);
while (!$file->eof()) {
$data = $file->fread(4096);
if (false === $data || '' === $data) {
break;
}
$this->write($data);
}
if ($l = $file->ftell() % 512) {
$l = 512 - $l;
$this->write(\pack("a{$l}", ''));
}
unset($file);
}
}
public function addFromString(string $name, string $contents): void
{
$this->start();
$this->writeEntryHeader(
$name,
static::TYPE_FILE,
\strlen($contents),
$this->stream_info->getPerms()
);
$this->write($contents);
if ($l = \strlen($contents) % 512) {
$l = 512 - $l;
$this->write(\pack("a{$l}", ''));
}
}
public function buildFromDirectory(string $dir, string $pattern = ''): array
{
$this->start();
\clearstatcache();
$dir = \rtrim($dir,'\\/') . '/';
$dirl = \strlen($dir);
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS /*| \FilesystemIterator::FOLLOW_SYMLINKS*/),
\RecursiveIteratorIterator::SELF_FIRST,
\RecursiveIteratorIterator::CATCH_GET_CHILD);
$ignore_paths = array();
foreach ($iterator as $name => $fileinfo) {
if ($fileinfo->isDir() && \is_file("{$fileinfo}/CACHEDIR.TAG")) {
// like: tar --exclude-caches -czf file.tgz *
$ignore_paths[] = \preg_quote($name . '/', '#');
continue;
}
if ($ignore_paths && \preg_match('#'.\implode('|',$ignore_paths).'#', $name)) {
continue;
}
if (!$pattern || \preg_match($pattern, $name)) {
$name = \substr($name, $dirl);
$this->addFile($name, null, $fileinfo);
}
}
return [];
}
protected function write(string $string): int
{
$length = \strlen($string);
$written = 0;
while ($written < $length) {
$bytes = \fwrite($this->stream, $written ? \substr($string, $written) : $string);
if (!$bytes) {
return $written ?: false;
}
$written += $bytes;
}
if ($this->gzip['h']) {
$this->gzip['l'] += $length;
\hash_update($this->gzip['h'], $string);
}
return $written;
}
protected function writeEntryHeader(string $name, int $type, int $size,
int $perm = 0, string $link = '', string $prefix = ''
): void
{
// handle long filename length
$paxdata = $paxname = '';
if (100 < \strlen($link)) {
$length = \strlen($link) + 11;
$length += \strlen($length);
$paxdata = "{$length} linkpath={$link}\n";
$link = '././@LongSymLink';
}
$l = \strlen($name);
if (($paxdata?90:100) < $l) {
// split into name and prefix
$p = \strpos($name, '/', \max(0, $l - 90));
if ($p && $p < $l-1) {
$file = \substr($name, $p+1);
$prefix = \substr($name, 0, $p);
$paxname = \preg_replace('#(^|/)([^/]+)$#', '$1PaxHeader/$2', $file);
} else {
$file = \basename($name);
if (static::TYPE_DIR === $type) {
$file .= '/';
}
$prefix = \dirname($name);
$paxname = 'PaxHeader/' . $file;
}
if (100 < \strlen($file) || 155 < \strlen($prefix)) {
// POSIX.1-2001/pax
$length = $l + 7;
$length += \strlen($length);
$paxdata = "{$length} path={$name}\n{$paxdata}";
if (static::TYPE_DIR === $type) {
$name = \substr($file, 0, 98) . '/';
} else {
$name = \substr($file, 0, 99);
}
} else {
// POSIX ustar
$name = $file;
}
$paxname = \substr($paxname, 0, 98);
}
if ($paxdata) {
/* Add?
$data .= "30 mtime=1461056595.149922432\n"
$data .= "30 ctime=1461056595.149922432\n"
*/
$this->writeUStarEntryHeader(
$paxname ?: \preg_replace('#(^|/)([^/]+)$#', '$1PaxHeader/$2', $name),
'x',
\strlen($paxdata),
$perm,
'',
$prefix
);
$l = 512 * \ceil(\strlen($paxdata) / 512);
$this->write(\pack("a{$l}", $paxdata));
}
$this->writeUStarEntryHeader($name, $type, $size, $perm, $link, $prefix);
}
// Writes Pre-POSIX.1-1988 (i.e. v7) and POSIX UStar headers
protected function writeUStarEntryHeader(string $name, int $type, int $size,
int $perm = 0, string $link = '', string $prefix = ''
): void
{
$uid = 0; // $this->stream_info->getOwner(),
$gid = 0; // $this->stream_info->getGroup(),
$mtime = $this->stream_info->getMTime();
$data = \pack('a100a8a8a8a12A12',
$name,
// values in octal
\sprintf("%06u ", \substr(\decoct($perm),-3)),
\sprintf("%06u ", \decoct($uid)),
\sprintf("%06u ", \decoct($gid)),
\sprintf("%011u ", \decoct($size)),
\sprintf("%011u", \decoct($mtime)));
$this->write($data);
$checksum = 0;
$i = 148;
while ($i--) {
$checksum += \ord($data[$i]);
}
$data = \pack('a1a100a6a2a32a32a8a8a155a12', "{$type}", $link, 'ustar', '00', '', '', '000000 ', '000000 ', $prefix, '');
$checksum += 256;
$i = 356;
while ($i--) {
$checksum += \ord($data[$i]);
}
$this->write(\pack('a8', \sprintf('%06u ', \decoct($checksum))) . $data);
}
}