From 2de4ebac5476b0b3402a01b07ce9e9d359275498 Mon Sep 17 00:00:00 2001 From: the-djmaze <3752035+the-djmaze@users.noreply.github.com> Date: Mon, 2 Nov 2026 00:54:36 +0100 Subject: [PATCH] Use custom TAR builder instead of PharData --- cli/release.php | 16 +- .../v/0.0.0/app/libraries/snappymail/tar.php | 271 ++++++++++++++++++ 2 files changed, 282 insertions(+), 5 deletions(-) diff --git a/cli/release.php b/cli/release.php index acafcc02b..fd17657b0 100755 --- a/cli/release.php +++ b/cli/release.php @@ -2,6 +2,7 @@ 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"; diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/tar.php b/snappymail/v/0.0.0/app/libraries/snappymail/tar.php index 80ae166a1..0098c69b0 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/tar.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/tar.php @@ -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); + } }