pear/archive_tar
Archive_Tar is a PHP package for creating, listing, extracting, and adding to tar files, with optional gzip, bz2, and Lzma2/xz compression support when the corresponding PHP extensions are loaded.
pear/archive_tar is a single-class, no-framework tar library. Despite its PEAR heritage it is installable through Composer (composer require pear/archive_tar), which is how you'll actually meet it in a Laravel app — either directly or as a transitive dependency (Drupal and various backup/export packages pull it in). There is no service provider, no config file, no facade: you new Archive_Tar(...) and call methods.
Pin at least ^1.4.14 — that is the release that fixed the symlink path-traversal CVE (CVE-2021-32610), and ^1.6.0 if you need to write archives containing single files larger than 8 GB.
Minimal first use case — extract an uploaded .tar.gz safely:
use Archive_Tar;
$upload = $request->file('bundle'); // bundle.tar.gz
$archive = storage_path('app/imports/'.$upload->hashName());
$upload->move(dirname($archive), basename($archive));
$tar = new Archive_Tar($archive, 'gz'); // 'gz' must be explicit
$tar->setErrorHandling(PEAR_ERROR_RETURN); // silence PEAR's print-to-output
$dest = storage_path('app/imports/'.Str::uuid());
if (! $tar->extract($dest)) {
throw new RuntimeException($tar->errorInfo());
}
Where to look first: the source is a single file, Archive/Tar.php (~2 000 lines). The public surface you will actually use is create(), add(), addString(), listContent(), extract(), extractList(), extractModify(), setPrefix(), setRemovePath(), setIgnoreList(), setErrorHandling(), errorInfo().
Wrap it in one small service class. You will call it from jobs, commands and controllers, so put setErrorHandling and error translation in one place:
final class TarService
{
public function make(string $path, ?string $compression = null): Archive_Tar
{
$tar = new Archive_Tar($path, $compression);
$tar->setErrorHandling(PEAR_ERROR_RETURN);
return $tar;
}
}
Bind it as a singleton in a service provider only if you must; it holds no state worth sharing, so app(TarService::class) on demand is enough.
Building release/backup bundles from disk. add() stores the path exactly as you pass it, so chdir() or setRemovePath() to keep absolute paths out of the archive:
$tar = $svc->make(storage_path('app/backups/app-'.now()->timestamp.'.tar.gz'), 'gz');
$tar->setRemovePath(base_path()); // strip /var/www/app from stored names
$tar->setIgnoreList(['#/node_modules/#', '#/\.git/#', '#/vendor/#']);
if (! $tar->create(['storage/app', 'public/uploads', '.env.example'])) {
Log::error('tar create failed', ['err' => $tar->errorInfo()]);
}
Note the ignore list takes PCRE patterns with delimiters, matched against the full path — not globs.
Selective extraction before trusting an upload. Combine listContent() with extractList() instead of calling extract() blind:
$entries = $tar->listContent(); // headers only — cheap
foreach ($entries as $e) {
if (str_starts_with($e['name'], '/') || str_contains($e['name'], '..')
|| ($e['typeflag'] ?? '') === '5' && $e['name'] === '') {
abort(422, 'Unsafe archive');
}
}
$tar->extractList(['package.json', 'src/'], $tmp); // extract a subset only
Strip a wrapper directory on extract (extractModify($dest, $removePath)) is the clean way to turn myplugin-1.2.3/src/... into src/... — no shelling out to tar --strip-components.
Generating archives in memory / streaming downloads. addString($contents, $nameInsideArchive) plus create() on a temp path is far simpler than trying to stream out of the class; then use response()->streamDownload() or Storage::put() on the result. Avoid holding a multi-hundred-MB archive string in PHP memory.
Queue anything large. extract() and create() are synchronous and CPU/IO bound. Put them in a job, write to a Str::uuid()-named temp directory under storage/app/tmp, and move the result into place on success.
bool. try/catch catches nothing. Check the return value and call errorInfo().PEAR_ERROR_PRINT, so a failed extract() can emit * Error extracting... into your HTTP response, corrupting JSON or a streamed download. Always call setErrorHandling(PEAR_ERROR_RETURN) first.new Archive_Tar('foo.tar.gz') without the second argument opens the file raw and parsing fails. Pass 'gz', 'bz2' or 'lzma2' (or true to infer from the extension, but explicit is safer). Each also needs ext-zlib / ext-bz2 / ext-xz present in the runtime — verify in your Docker image before shipping.add() stores the argument verbatim. Use setRemovePath() or chdir(); otherwise your backup contains /var/www/html/..., which makes extraction on another host a mess.phar:// names. Hardening landed progressively (1.4.4 blocked phar://, 1.4.9 added a symlink restriction, 1.4.14 fixed the traversal CVE). Older versions in a transitive composer.lock are a real risk — check composer why pear/archive_tar and bump it..tmp sibling; two queued jobs writing the same target name will clobber each other. Use per-job unique filenames.umask and the filesystem can override it. Don't rely on it for executable bits.listContent() reads headers only and is safe for big archives; addString() and in-memory creation are not. 1.6.0 improved large-file writes, but the class still buffers blocks — cap upload sizes at the validation layer.Storage fake, re-open it, and assert on listContent() names/sizes..tar/.tar.gz work inside a Laravel app, PHP's built-in PharData handles list/extract/write with zero extra dependencies, and the tar binary via Symfony\Component\Process is faster and more forgiving. Reach for Archive_Tar when you specifically need its extractList()/extractModify()/setRemovePath() conveniences or you're already carrying it transitively — not as a first choice.How can I help you explore Laravel packages today?