Oihana PHP

tar

Table of Contents

Functions

assertTar()  : bool
Validates that a file is a tar archive (compressed or uncompressed).
hasTarExtension()  : bool
Checks if a file has a tar-related extension.
hasTarMimeType()  : bool
Checks if a file has a tar-related extension.
tar()  : string
Creates a tar archive from one or more files and/or directories.
tarBinary()  : string|null
The system `tar` this library is willing to use, or null.
tarBinaryIsUsable()  : bool
Whether a candidate is an executable GNU tar.
tarCompressionOfFile()  : string|null
Reports how an archive is compressed, read from its own first bytes.
tarCopyMovedLine()  : string
Writes the sentence that names the files tar saw move while it copied them.
tarCopyOutcome()  : array{ok: bool, moved: list, reason: string}
Tells what a copy made by {@see tarCopyScript()} amounts to.
tarCopyScript()  : string
Builds the bash script that copies part of a directory into another through two GNU tar.
tarDirectory()  : string
Creates a tar archive from a directory with specified compression.
tarEntries()  : array<int, array{base: string, name: string, directory: bool, path: string}>
Works out what an archive is to contain, and under which names.
tarEntriesByBase()  : array<string, array<int, string>>
Groups entries by the directory their names are relative to.
tarExcludesPath()  : bool
Tells whether GNU tar's `--exclude` patterns would leave a path out of an archive.
tarFileInfo()  : array{is_valid?: bool, extension?: string, mime_type?: string|null, compression?: string|null, file_count?: int|null, total_size?: int|null}
Retrieves detailed information about a tar archive file.
tarIsCompressed()  : bool
Checks if a given tar file is compressed.
tarLinkEntries()  : array<int, array{name: string, target: string, hard: bool}>
Reads the headers of an uncompressed tar archive and returns the link entries it holds.
tarHeaderSize()  : int
The size a tar header declares.
tarPaxRecords()  : array<string, string>
The records of a pax extended header.
tarReadBlock()  : string|null
One whole 512-byte block, or `null` when the stream has no more to give.
tarProcessEnvironment()  : array<string, string>
The environment every `tar` this library runs is given.
tarWithBinary()  : void
Builds an archive with the system `tar`.
tarCompressionFlag()  : string|null
Whether the system tar can produce this compression, and with which flag.
tarCompressorExists()  : bool
Whether the compressor `tar` would shell out to is installed.
tarBinaryHandles()  : bool
Whether the binary engine can take this archive on.
tarWritableCompressions()  : array<int, string>
The compressions {@see tar()} can actually write **on this host**, in a stable order.
untar()  : true|array<string|int, string>
Extracts a tar archive file into a specified output directory.
untarAssertExtractable()  : array<int, string>
Checks what an archive holds against what the caller allows, before anything is written.
untarBinaryHandles()  : bool
Whether the system `tar` can extract this archive here.
untarEntries()  : array<int, array{name: string, size: int, directory: bool}>
Lists what an archive holds, without extracting it.
untarUniqueAlias()  : string|null
Gives a compressed archive a second, uniquely named path, so decompressing it twice in one process does not collide.
untarWithBinary()  : void
Extracts an archive with the system `tar`.
validateTarStructure()  : bool
Validates the internal structure of a tar file.

Functions

assertTar()

Validates that a file is a tar archive (compressed or uncompressed).

assertTar(string $filePath[, bool $strictMode = false ]) : bool
Parameters
$filePath : string

Path to the file to validate.

$strictMode : bool = false

If true, performs deep validation using file contents. If false, only checks extension and basic MIME type.

Tags
throws
FileException

If the file does not exist or cannot be read.

example

Basic validation using file extension and MIME type:

$isValid = isTarFile('/path/to/archive.tar');

Strict validation with file content inspection:

$isValid = isTarFile('/path/to/archive.tar.gz', true);

Validation failure for non-tar file:

$isValid = isTarFile('/path/to/image.jpg');
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
bool —

True if the file is a valid tar archive, false otherwise.

hasTarExtension()

Checks if a file has a tar-related extension.

hasTarExtension(string $filePath[, array<string|int, string> $tarExtensions = [ FileExtension::TAR, FileExtension::TGZ, FileExtension::GZ, FileExtension::TAR_GZ, FileExtension::TAR_BZ2, FileExtension::BZ2, // Written by the system tar since 1.5.0, so they have to be recognised as tar // archives or the library could no longer read back what it writes. Taken from // TarExtension, which is the catalogue of tar extensions — `.gz` and `.bz2` above // are not, which is exactly why they surprise. TarExtension::TAR_XZ, TarExtension::TXZ, TarExtension::TAR_ZST, ] ]) : bool
Parameters
$filePath : string

Path to the file.

$tarExtensions : array<string|int, string> = [ FileExtension::TAR, FileExtension::TGZ, FileExtension::GZ, FileExtension::TAR_GZ, FileExtension::TAR_BZ2, FileExtension::BZ2, // Written by the system tar since 1.5.0, so they have to be recognised as tar // archives or the library could no longer read back what it writes. Taken from // TarExtension, which is the catalogue of tar extensions — `.gz` and `.bz2` above // are not, which is exactly why they surprise. TarExtension::TAR_XZ, TarExtension::TXZ, TarExtension::TAR_ZST, ]

Optional list of valid tar-related extensions. Defaults to common tar and compressed tar extensions:

  • .tar
  • .tgz
  • .gz
  • .tar.gz
  • .tar.bz2
  • .bz2
Tags
author

Marc Alcaraz (ekameleon)

since
1.0.0
example

Check a simple tar file:

var_dump(hasTarExtension('/path/to/archive.tar')); // bool(true)

Check a gzipped tar file:

var_dump(hasTarExtension('/path/to/archive.tar.gz')); // bool(true)

Check a file with .tgz extension:

var_dump(hasTarExtension('/path/to/archive.tgz')); // bool(true)

Check a file with unsupported extension:

var_dump(hasTarExtension('/path/to/archive.zip')); // bool(false)

Check a file with double extension .tar.bz2:

var_dump(hasTarExtension('/path/to/archive.tar.bz2')); // bool(true)
Return values
bool —

True if the file has a recognized tar extension.

hasTarMimeType()

Checks if a file has a tar-related extension.

hasTarMimeType(string $filePath[, array<string|int, string> $mimeTypes = [ 'application/x-tar', 'application/tar', 'application/gzip', 'application/x-gzip', 'application/x-bzip2', 'application/bzip2', 'application/x-compressed-tar', // What `finfo` reports for the two formats the system tar writes since 1.5.0, // verified on real archives: `application/x-xz` and `application/zstd`. 'application/x-xz', 'application/zstd', ] ]) : bool

This function inspects the MIME type of the given file against a list of valid tar-related MIME types to determine if the file is a tar archive.

It is a thin wrapper around hasMimeType pre-configured with the common tar MIME types.

Parameters
$filePath : string

Path to the file.

$mimeTypes : array<string|int, string> = [ 'application/x-tar', 'application/tar', 'application/gzip', 'application/x-gzip', 'application/x-bzip2', 'application/bzip2', 'application/x-compressed-tar', // What `finfo` reports for the two formats the system tar writes since 1.5.0, // verified on real archives: `application/x-xz` and `application/zstd`. 'application/x-xz', 'application/zstd', ]

Optional list of valid tar MIME types. Defaults to common tar and compressed tar types:

  • 'application/x-tar'
  • 'application/tar'
  • 'application/gzip'
  • 'application/x-gzip'
  • 'application/x-bzip2'
  • 'application/bzip2'
  • 'application/x-compressed-tar'
Tags
author

Marc Alcaraz (ekameleon)

since
1.0.0
example

Check if a .tar.gz file is a tar archive:

$result = hasTarMimeType('/path/to/archive.tar.gz');
var_dump($result); // bool(true) or bool(false)

Check a file with a custom list of MIME types:

$customTypes = ['application/x-tar', 'application/x-custom-tar'];
$result = hasTarMimeType('/path/to/custom.tar', $customTypes);

Check a non-existent file (returns false):

$result = hasTarMimeType('/path/to/missing.tar');
var_dump($result); // bool(false)
Return values
bool —

True if the file exists and its MIME type matches one of the given tar MIME types.

tar()

Creates a tar archive from one or more files and/or directories.

tar(string|array<string|int, string> $paths[, string|null $outputPath = null ][, string|null $compression = CompressionType::GZIP ][, string|null $preserveRoot = null ]) : string

This function supports adding multiple paths (files or directories) to a tar archive, with optional compression (gzip, bzip2, or none). It can preserve the root directory structure inside the archive, and generates a unique temporary archive if no output path is specified.

Empty directories are preserved in the archive.

Parameters
$paths : string|array<string|int, string>

Absolute path(s) to file(s) or directory(ies) to include in the archive.

$outputPath : string|null = null

Optional full path to the final archive file to create. If null, an automatic unique filename with timestamp is generated in the system temp directory.

$compression : string|null = CompressionType::GZIP

Compression type to use on the tar archive. Supported values are defined in CompressionType, defaults to CompressionType::GZIP.

$preserveRoot : string|null = null

If set, paths inside the archive will be stored relative to this directory, allowing to preserve directory structure when extracting. Must be an absolute path.

Tags
throws
FileException

If any of the provided paths does not exist or is invalid.

UnsupportedCompressionException

If the requested compression type is not supported by the system.

DirectoryException

If the temporary directory cannot be created or accessed.

RuntimeException

If no files are added to the archive or if an error occurs during creation, including inability to rename temporary files.

see
CompressionType
example

Archive a single file, auto-named, gzip compressed (default):

$tarPath = tar('/var/www/html/index.php');

Archive a directory with bzip2 compression:

$tarPath = tar('/var/www/html', '/tmp/site.tar.bz2', CompressionType::BZIP2);

Archive multiple files with no compression:

$tarPath = tar
(
   ['/etc/hosts', '/etc/hostname'],
   '/tmp/config.tar',
   CompressionType::NONE
);

Archive directory with root preserved (relative paths):

$tarPath = tar
(
    '/var/www/html/project',
    '/tmp/project.tar.gz',
    CompressionType::GZIP,
    '/var/www/html'
);
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
string —

Returns the full path to the created tar archive file.

tarBinary()

The system `tar` this library is willing to use, or null.

tarBinary([bool $refresh = false ]) : string|null

PharData writes tar archives in pure PHP, and on anything but a toy tree it is not viable: measured against GNU tar on the same 96 MB / 7 554-file directory, producing the same 17 MB archive, it took 317 seconds against 1.63 — and the gap widens with size, because it writes the tar and then reads the whole thing back to compress it. It also refuses any path component longer than 100 bytes, the ustar limit, which a single file of a stock WordPress plugin set is enough to hit.

So the work is handed to the system binary when one is there. Which one matters:

  • GNU tar stores names as raw bytes, exactly as PharData does. Verified on a tree of accented, CJK, quoted and spaced names, plus a symlink and an empty directory: the entry lists are identical.
  • bsdtar, which is what macOS ships as /usr/bin/tar, converts filenames to Unicode NFD. été.txt becomes e´te´.txt inside the archive. An archive written on a Mac and restored on a server would carry different names than the originals — for a site with accented media, different URLs. Speed is not worth that, and a Mac is a development machine rather than a backup target.
  • BusyBox tar is a reduced implementation whose fidelity has not been measured here.

Hence the rule: the binary is used when it identifies itself as GNU tar, and PharData carries the rest. Slower and identical beats faster and subtly different.

OIHANA_TAR_BINARY overrides the search: a path to use one in particular, or an empty value to force the PharData path — which is how the test suite exercises both engines on the same fixtures.

Parameters
$refresh : bool = false

Whether to look again instead of reusing the previous answer.

Tags
example
$binary = tarBinary() ;

echo $binary === null
    ? 'archives are built in PHP, which is slow on large trees'
    : sprintf( 'archives are built by %s' , $binary ) ;
author

Marc Alcaraz (ekameleon)

since
1.3.0
Return values
string|null —

The binary to run, or null when the archive is to be built in PHP.

tarBinaryIsUsable()

Whether a candidate is an executable GNU tar.

tarBinaryIsUsable(string $candidate) : bool

Asked by running it rather than by trusting its name: /usr/bin/tar is GNU tar on Linux and bsdtar on macOS, and the two do not treat filenames the same way.

Parameters
$candidate : string

The path to test.

Tags
author

Marc Alcaraz (ekameleon)

since
1.3.0
Return values
bool —

True when the binary runs and identifies itself as GNU tar.

tarCompressionOfFile()

Reports how an archive is compressed, read from its own first bytes.

tarCompressionOfFile(string $tarFile) : string|null

Not from its name. tarIsCompressed() answers the same question from the extension, which is all that is needed to pick a code path for an archive this library wrote — but an archive handed to untar() may be named anything, and the system tar has to be told nothing it cannot verify. Three bytes settle it.

Scope: gzip, bzip2, xz, zstd, or none — the compressions this library writes (TarExtension::getExtensionForCompression()) and the ones hasTarExtension lets through. xz and zstd joined in 1.5.0, and this function is one of the six places that had to follow for an archive to be readable as well as writable: a format written and not read back is the defect that lot was fixing.

Parameters
$tarFile : string

Path to the archive.

Tags
example
use function oihana\files\archive\tar\tarCompressionOfFile;

tarCompressionOfFile('/tmp/site.tar.gz') ;  // 'gzip'
tarCompressionOfFile('/tmp/site.tar.zst') ; // 'zstd'
tarCompressionOfFile('/tmp/site.tar') ;     // null
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
string|null —

CompressionType::GZIP, CompressionType::BZIP2, or null for an uncompressed archive — and for one that cannot be read, since an unreadable file is not a compressed one.

tarCopyMovedLine()

Writes the sentence that names the files tar saw move while it copied them.

tarCopyMovedLine(array<string|int, string> $moved, string $what[, int|null $limit = null ]) : string

The moved list of tarCopyOutcome(), put in words for a terminal, a log or a mail. A terminal can take every file; a mail or a report should take a few: a cache folder that keeps changing must not make a message of a thousand lines. So a limit names that many at most, and says how many it left out. The count is always of every file.

Parameters
$moved : array<string|int, string>

The files, as tar named them.

$what : string

What was being copied, e.g. content or the site root.

$limit : int|null = null

How many files to name at most, one at least; null names them all.

Tags
example
use function oihana\files\archive\tar\tarCopyMovedLine;

tarCopyMovedLine( [ 'content/a.log' ] , 'content' ) ;
// '1 file changed while content was copied, and was copied as found: content/a.log'

tarCopyMovedLine( [ 'a' , 'b' , 'c' , 'd' ] , 'content' , 2 ) ;
// '4 files changed while content was copied, and were copied as found: a, b … and 2 more'
author

Marc Alcaraz (ekameleon)

since
1.7.0
Return values
string —

The sentence; an empty string when no file moved.

tarCopyOutcome()

Tells what a copy made by {@see tarCopyScript()} amounts to.

tarCopyOutcome(int $status, string $error) : array{ok: bool, moved: list, reason: string}

Exit code 1 is accepted when, and only when, every line tar wrote says that a file moved while it was read. GNU tar has three forms of it:

  • tar: <path>: file changed as we read it — a file written to during the copy;
  • tar: <path>: File removed before we read it — a file deleted between the listing and the reading, a cache entry for instance;
  • tar: <path>: File shrank by <n> bytes; padding with zeros — a file cut short while it was read, a log being truncated. Its copy is completed with zeros.

A copy taken from a directory in use is never a frozen picture of it; these three say which files were caught in motion, and they are named so a caller can report them. One other line, and the copy fails: tar exits 1 for nothing else, and a message this function has not been taught is not one to wave through.

Parameters
$status : int

What the script exited with: 0, 1 or 2 from the reading tar, 3 when the writing one failed.

$error : string

What the two tar wrote on standard error.

Tags
example
use function oihana\files\archive\tar\tarCopyOutcome;

tarCopyOutcome( 0 , '' ) ;
// [ 'ok' => true , 'moved' => [] , 'reason' => '' ]

tarCopyOutcome( 1 , "tar: htdocs/debug.log: file changed as we read it\n" ) ;
// [ 'ok' => true , 'moved' => [ 'htdocs/debug.log' ] , 'reason' => '' ]

tarCopyOutcome( 3 , "tar: htdocs/a.jpg: Cannot write: No space left on device\n" ) ;
// [ 'ok' => false , 'moved' => [] , 'reason' => 'the copy could not be written to its destination: tar: htdocs/a.jpg: …' ]
author

Marc Alcaraz (ekameleon)

since
1.7.0
Return values
array{ok: bool, moved: list, reason: string} —

Whether the copy stands, the paths that moved while they were read, and why the copy failed when it did.

tarCopyScript()

Builds the bash script that copies part of a directory into another through two GNU tar.

tarCopyScript(string $source, array<string|int, string> $members, string $destination[, array<string|int, string> $exclude = [] ]) : string

tar -cf - | tar -xf - rather than cp, because tar never reads what it excludes: a cache or an archive left in the tree costs nothing to leave out. What this script adds is the reading of the two exit codes, which set -o pipefail would merge into one:

  • the tar that reads exits 0, 1 when files moved while it read them — modified, removed before being read, or shrunk — and 2 for a fatal error. On a directory that is in use, 1 happens by chance: the same copy passes a minute later. So the script exits with that code, for tarCopyOutcome() to judge from what tar said;
  • the tar that writes must end in 0, always: a full disk shows there. When it does not, the script exits 3, a code the reading tar never uses for this.

It runs under LC_ALL=C: a tar that speaks the language of the machine reports a moved file in that language, and tarCopyOutcome() reads the English messages of GNU tar. The script runs tar as the PATH finds it, and relies on bash for PIPESTATUS: run it with bash -c.

Every path and pattern is escaped for the shell.

Parameters
$source : string

The directory the members are read from.

$members : array<string|int, string>

What is copied, relative to $source.

$destination : string

The directory the copy is written to. It must exist.

$exclude : array<string|int, string> = []

--exclude patterns, as GNU tar reads them (tarExcludesPath()).

Tags
example
use function oihana\files\archive\tar\tarCopyOutcome;
use function oihana\files\archive\tar\tarCopyScript;

$script = tarCopyScript( '/srv/app' , [ 'content' ] , '/tmp/copy' , [ 'content/cache' ] ) ;

$process = proc_open( [ 'bash' , '-c' , $script ] , [ 2 => [ 'pipe' , 'w' ] ] , $pipes ) ;
$error   = stream_get_contents( $pipes[ 2 ] ) ;
fclose( $pipes[ 2 ] ) ;

$outcome = tarCopyOutcome( proc_close( $process ) , $error ) ;
author

Marc Alcaraz (ekameleon)

since
1.7.0
Return values
string —

The script, for bash -c.

tarDirectory()

Creates a tar archive from a directory with specified compression.

tarDirectory(string $directory[, string|null $compression = CompressionType::GZIP ][, string|null $outputPath = null ][, array<string|int, mixed> $options = [] ]) : string

This function creates a compressed (or uncompressed) tar archive from the given directory. It supports filtering files by exclude patterns, by a callback filter function, and adding optional metadata saved as .metadata.json inside the archive.

If no filters or metadata are provided, it simply creates the archive directly from the directory. Otherwise, it copies filtered files to a temporary directory and archives from there.

Parameters
$directory : string

The source directory to archive.

$compression : string|null = CompressionType::GZIP

Compression type (e.g. gzip, bzip2, none). Default is gzip compression.

$outputPath : string|null = null

Optional output archive path. If null, defaults to directory name plus extension based on compression.

$options : array<string|int, mixed> = []

Additional options:

  • exclude => string[] list of glob patterns or file names to exclude
  • filter => callable|null a function (string $filepath): bool
  • metadata => array<string, string> extra metadata to embed in .metadata.json
Tags
throws
DirectoryException

If the source directory does not exist or is inaccessible.

FileException

If there are issues writing files or archives.

UnsupportedCompressionException

If an unsupported compression type is specified.

RuntimeException

If no files match filtering criteria.

example
// Create a gzip compressed tar archive from directory /var/www/html
$archive = tarDirectory('/var/www/html');
echo $archive; // /var/www/html.tar.gz

// Create a bz2 compressed archive, excluding .git and node_modules folders
$archive = tarDirectory(
    '/var/www/html',
    CompressionType::BZIP2,
    null,
    [
        TarOption::EXCLUDE => ['.git', 'node_modules'],
    ]
);

// Create an archive with a custom filter callback and add metadata
$archive = tarDirectory(
    '/var/www/html',
    CompressionType::NONE,
    '/backups/html_backup.tar',
    [
        TarOption::FILTER => function(string $filePath): bool {
            // Only include PHP files
            return str_ends_with($filePath, '.php');
        },
        TarOption::METADATA => [
            'createdBy' => 'admin',
            'description' => 'Backup of PHP source files',
        ],
    ]
);
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
string —

Returns the full path to the created archive file.

tarEntries()

Works out what an archive is to contain, and under which names.

tarEntries(array<string|int, string> $paths[, string|null $preserveRoot = null ]) : array<int, array{base: string, name: string, directory: bool, path: string}>

Written once and used by both engines, because the naming is the whole compatibility contract: an archive is only interchangeable with the ones written before it if the entries are called the same thing. Two copies of these rules would drift, and the drift would only show up the day someone tried to restore an old backup.

Each entry carries the directory it is named relative to, so the caller can either add it to a PharData or hand the list to tar -C <base>.

Parameters
$paths : array<string|int, string>

The real paths to archive, each proven to exist.

$preserveRoot : string|null = null

The directory names are taken relative to, when given.

Tags
author

Marc Alcaraz (ekameleon)

since
1.3.0
Return values
array<int, array{base: string, name: string, directory: bool, path: string}> —

base is what name is relative to, path is where the content really is.

tarEntriesByBase()

Groups entries by the directory their names are relative to.

tarEntriesByBase(array<int, array{base: string, name: string, directory: bool, path: string}> $entries) : array<string, array<int, string>>

tar is told a base with -C and reads the names from standard input, so one invocation covers one base. Archiving several unrelated paths at once therefore takes several.

Parameters
$entries : array<int, array{base: string, name: string, directory: bool, path: string}>
Tags
author

Marc Alcaraz (ekameleon)

since
1.3.0
Return values
array<string, array<int, string>> —

The names to archive, keyed by base directory.

tarExcludesPath()

Tells whether GNU tar's `--exclude` patterns would leave a path out of an archive.

tarExcludesPath(string $path, array<string|int, string> $patterns) : bool

An archive made with tar --exclude=<pattern> … is missing two kinds of things compared with the tree it was made from: what was added since, and what a pattern left out on purpose. A caller that compares the two — to restore a tree to its archive, for instance — has to tell them apart, or it removes what was never meant to be archived.

This reads the patterns the way GNU tar reads them by default:

  • unanchored: a pattern may match from any / of the member name, so cache also matches app/cache ;
  • leading directory: a pattern matching a directory leaves out everything under it, so cache matches app/cache/a.bin ;
  • wildcards cross a slash: * matches / too, as fnmatch() does with no flags.

Leading and trailing slashes of the path and the patterns are ignored, and an empty pattern matches nothing. Erring wide is the safe direction for the usual caller: a path taken for excluded is kept, never removed.

Parameters
$path : string

A path relative to the directory tar was run from, e.g. content/cache/a.bin.

$patterns : array<string|int, string>

The --exclude patterns.

Tags
see
https://www.gnu.org/software/tar/manual/html_node/controlling-pattern_002dmatching.html
example
use function oihana\files\archive\tar\tarExcludesPath;

tarExcludesPath( 'content/cache/a.bin' , [ 'cache' ] ) ;         // true  (unanchored, leading directory)
tarExcludesPath( 'content/logs/a.log'  , [ 'content/*.log' ] ) ; // true  (* crosses the slash)
tarExcludesPath( 'content/cached.txt'  , [ 'cache' ] ) ;         // false (a segment, not a prefix)
author

Marc Alcaraz (ekameleon)

since
1.7.0
Return values
bool —

True when one of the patterns leaves the path out.

tarFileInfo()

Retrieves detailed information about a tar archive file.

tarFileInfo(string $filePath[, bool $strictMode = false ]) : array{is_valid?: bool, extension?: string, mime_type?: string|null, compression?: string|null, file_count?: int|null, total_size?: int|null}

This function inspects the given tar file to determine its validity, compression type, MIME type, number of contained files, and total size of the contents.

It uses the PharData class to count files and calculate total size when the tar is valid.

Parameters
$filePath : string

Absolute path to the tar archive file to inspect.

$strictMode : bool = false

When true, enables strict validation of the tar file structure via assertTar(). Default is false for a more lenient check.

Tags
throws
FileException

If the provided file does not exist or is not accessible.

see
assertTar()
example
$info = tarFileInfo( '/archives/sample.tar' );
print_r( $info );

$info = tarFileInfo( '/archives/compressed.tar.gz' );
echo $info['compression']; // 'gzip'

$info = tarFileInfo( '/bad/path.tar' );
var_dump( $info['isValid'] ); // false

// Strict mode
$info = tarFileInfo( '/archives/sample.tar' , true );
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
array{is_valid?: bool, extension?: string, mime_type?: string|null, compression?: string|null, file_count?: int|null, total_size?: int|null} —

Returns an associative array with:

  • isValid: Whether the tar file is valid according to assertTar().
  • extension: File extension (lowercase) extracted from the path.
  • mimeType: MIME type detected via finfo.
  • compression: Compression type detected (gzip, bzip2, or none).
  • fileCount: Number of files inside the tar (if valid), otherwise null.
  • totalSize: Sum of sizes (in bytes) of all files inside (if valid), otherwise null.

tarIsCompressed()

Checks if a given tar file is compressed.

tarIsCompressed(string $tarFile) : bool

This function determines whether the file name indicates a compressed tar archive based on common compressed tar extensions such as .tar.gz, .tgz, .tar.bz2, or .tbz2.

Note: This function only inspects the file name extension, not the actual file contents.

Parameters
$tarFile : string

The path or filename of the tar archive.

Tags
example
var_dump( tarIsCompressed( 'archive.tar.gz'   ) ); // true
var_dump( tarIsCompressed( 'archive.tgz'      ) ); // true
var_dump( tarIsCompressed( 'archive.tar.bz2'  ) ); // true
var_dump( tarIsCompressed( 'archive.tar.xz'   ) ); // true
var_dump( tarIsCompressed( 'archive.tar.zst'  ) ); // true
var_dump( tarIsCompressed( 'archive.tbz2'     ) ); // true
var_dump( tarIsCompressed( 'archive.tar'      ) ); // false
var_dump( tarIsCompressed( 'archive.zip'      ) ); // false
var_dump( tarIsCompressed( 'README.md'        ) ); // false
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
bool —

True if the file is recognized as a compressed tar archive, false otherwise.

tarLinkEntries()

Reads the headers of an uncompressed tar archive and returns the link entries it holds.

tarLinkEntries(string $tarFile[, string|null $compression = null ]) : array<int, array{name: string, target: string, hard: bool}>

PharData is the only reader this library has when there is no GNU tar, and it cannot see a link: a link entry comes back from its iterator as a regular file of size 0, with isLink() answering false, and extractTo() writes an empty file in its place. Asking it is therefore not an option, and the headers are read here instead — they are plain, fixed and documented.

A compressed archive is read through its stream wrapper, which is the only way to see its links at all: PharData::decompress() rewrites the archive to decompress it, and since it cannot represent a link it writes a regular empty file in its place. The link is gone from the .tar it produces, so the compressed stream has to be read before it, not after.

Both kinds of link are reported, '2' (symbolic) and '1' (hard), along with GNU long names ('L', 'K') and the path/linkpath records of a pax header ('x'), since any of them can carry the name or the target of the entry that follows.

Parameters
$tarFile : string

Path to a tar archive.

$compression : string|null = null

Its compression, from CompressionType, or null when the archive is a plain .tar. Only gzip and bzip2 can be read here, since those are the two PHP has a stream wrapper for.

Tags
throws
RuntimeException

If the file cannot be read, is not a tar archive, or holds a block that is not a header.

example
use function oihana\files\archive\tar\tarLinkEntries;

foreach ( tarLinkEntries( '/tmp/site.tar' ) as $link )
{
    // [ 'name' => 'wp-content/uploads' , 'target' => '/var/data/uploads' , 'hard' => false ]
}
author

Marc Alcaraz (ekameleon)

since
1.6.0
Return values
array<int, array{name: string, target: string, hard: bool}> —

The link entries, in the order the archive holds them.

tarHeaderSize()

The size a tar header declares.

tarHeaderSize(string $header) : int

Octal, as the format has always had it, unless the field's high bit is set: GNU writes a base-256 integer there for a size the twelve octal digits cannot hold.

Parameters
$header : string

One 512-byte tar header.

Tags
author

Marc Alcaraz (ekameleon)

since
1.6.0
Return values
int —

The declared size in bytes.

tarPaxRecords()

The records of a pax extended header.

tarPaxRecords(string $data) : array<string, string>

Each record is "<length> <keyword>=<value>\n", and the length counts itself, so the records are walked by the length they declare rather than split on a separator a value may contain.

Parameters
$data : string

The pax header's data blocks, trimmed to its declared size.

Tags
author

Marc Alcaraz (ekameleon)

since
1.6.0
Return values
array<string, string> —

The records, keyed by keyword.

tarReadBlock()

One whole 512-byte block, or `null` when the stream has no more to give.

tarReadBlock(resource $handle) : string|null

fread() on a compressed stream answers with what the inflater has ready rather than what was asked for, so a block is gathered until it is whole.

Parameters
$handle : resource

An open archive stream.

Tags
author

Marc Alcaraz (ekameleon)

since
1.6.0
Return values
string|null —

The block, or null at the end of the stream.

tarProcessEnvironment()

The environment every `tar` this library runs is given.

tarProcessEnvironment() : array<string, string>

Two variables, and each one is there for a measured reason.

LC_ALL=C so the output keeps one shape. tar -tv prints a date column, and untarEntries() reads the member name as everything after the time — a locale that spells dates differently would make that parse fail on a sound archive.

PATH, passed through from this process, because proc_open() given an environment replaces the whole of it rather than adding to it. And tar compresses nothing itself: --xz makes it spawn xz through a shell. With no PATH, that shell falls back to a built-in default — /usr/bin:/bin on a Mac — so gzip and bzip2 were found and xz and zstd, installed by Homebrew under /opt/homebrew/bin, were not. Measured on 2026-10-24:

/bin/sh: xz: command not found
tar: Child returned status 127

…on a host where tarCompressorExists() had just found xz on this process's own PATH and answered yes. The check looked at one environment and the work ran in another. That it went unnoticed until now is only because tar() refused xz before ever reaching the binary.

Tags
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
array<string, string>

tarWithBinary()

Builds an archive with the system `tar`.

tarWithBinary(string $binary, array<int, array{base: string, name: string, directory: bool, path: string}> $entries, string $finalPath, string $compression) : void

The entry names are computed in PHP by tarEntries() and handed over verbatim, with --no-recursion, so tar walks nothing and decides nothing: it writes exactly the list it is given, under exactly the names PharData would have used. That is what keeps archives written by either engine interchangeable.

The list travels on standard input, NUL-separated. Passed as arguments it would break on any real site — a stock WordPress install is sixteen thousand paths, far past ARG_MAX — and NUL separation is the only form that survives newlines and quotes in filenames.

Parameters
$binary : string

The tar to run.

$entries : array<int, array{base: string, name: string, directory: bool, path: string}>

What to archive.

$finalPath : string

Where the archive goes.

$compression : string

A CompressionType value.

Tags
throws
RuntimeException

If tar cannot be started, or refuses the archive.

author

Marc Alcaraz (ekameleon)

since
1.3.0

tarCompressionFlag()

Whether the system tar can produce this compression, and with which flag.

tarCompressionFlag(string $compression) : string|null
Parameters
$compression : string

A CompressionType value.

Tags
author

Marc Alcaraz (ekameleon)

since
1.3.0
Return values
string|null —

The flag, null for an uncompressed archive.

tarCompressorExists()

Whether the compressor `tar` would shell out to is installed.

tarCompressorExists(string $compression) : bool

tar does not compress: it pipes through gzip, bzip2, xz. PharData does it in PHP, through the bundled extensions — so a machine with ext-bz2 and no bzip2 program used to produce a bzip2 archive and would stop. Asked before the engine is chosen, so that host keeps the engine that works for it instead of receiving an error where it had a file.

Parameters
$compression : string

A CompressionType value.

Tags
author

Marc Alcaraz (ekameleon)

since
1.3.0
Return values
bool —

True when nothing external is needed, or when what is needed is there.

tarBinaryHandles()

Whether the binary engine can take this archive on.

tarBinaryHandles(array<int, array{base: string, name: string, directory: bool, path: string}> $entries, string $compression) : bool

It takes the ones it can reproduce exactly, and leaves the rest to PharData — decided before anything is written, never after something has failed.

Archiving several paths that do not share a parent needs one tar invocation per parent, and an archive cannot be appended to once compressed. That case is rare — it means passing unrelated paths with no preserveRoot — and PharData already handles it correctly, so it stays there rather than justifying a second, less-travelled code path.

Parameters
$entries : array<int, array{base: string, name: string, directory: bool, path: string}>
$compression : string
Tags
author

Marc Alcaraz (ekameleon)

since
1.3.0
Return values
bool

tarWritableCompressions()

The compressions {@see tar()} can actually write **on this host**, in a stable order.

tarWritableCompressions() : array<int, string>

Not a constant list, because it is not a property of this library: it is a property of the machine. Two engines contribute, and they do not cover the same formats.

  • PharData compresses through PHP extensions, so it offers gzip with ext-zlib and bzip2 with ext-bz2, and nothing else — Phar knows no other.
  • The system tar pipes through a program, so it offers whatever it can find: gzip, bzip2, xz and zstd. Which means a host can write .tar.zst and the one next to it cannot, with the same code and the same version of this library.

none is always there.

Why this is a function and not documentation. tar() used to refuse xz with "Compression type 'xz' is not supported", raised by a helper assembling a temporary path, seventeen lines before the engines had even been looked at — a sentence about this library where the truth was about the host. The refusal now names what is missing and what would work here, and this is where it reads it. It is also what a doctor command should print: a host that silently writes .tar.gz where the operator expected .tar.zst has nothing else to tell them.

Deliberately absent: lzma, which xz superseded and whose --lzma flag tarCompressionFlag() still carries for a caller going straight to tarWithBinary(), and lz4, for which GNU tar has no flag at all.

Tags
example
use function oihana\files\archive\tar\tarWritableCompressions;

tarWritableCompressions() ;
// → ['none','gzip','bzip2','xz','zstd'] on a Debian with xz-utils and zstd
// → ['none','gzip','bzip2']             on a host with no GNU tar
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
array<int, string> —

Values of CompressionType, none first.

untar()

Extracts a tar archive file into a specified output directory.

untar(string $tarFile, string $outputPath[, array{dryRun?: bool, keepPermissions?: bool, overwrite?: bool, maxExtractedSize?: int|null} $options = [] ]) : true|array<string|int, string>

This function supports regular and compressed tar files (.tar, .tar.gz, .tar.bz2). It can perform a dry run to preview extracted files, optionally preserve file permissions, and control overwriting of existing files.

Which engine extracts

The system tar when there is a GNU one and the decompressor it needs (untarBinaryHandles()), PharData otherwise. The choice is made before anything is written and is not revisited, as it is for writing in tar().

It is not a question of speed: PharData extracts a plain archive at a comparable pace. It is that decompressing with PharData means having PharData write a new .tar first, and its writer refuses any path component over 100 bytes — the ustar limit. Since 1.3.0 tar() delegates to GNU tar, which stores such names without difficulty, so a .tar.gz this library wrote could fail to extract through this one. A stock WordPress carries such a path. The rebuild also costs about twice the archive in memory, and an uncompressed copy on disk beside it.

Both engines honour the four options below and produce the same tree, which is why the guards live in one place, untarAssertExtractable().

Extracting the same archive twice, on the PharData path

It used to fail, with "a phar with that name already exists": PharData::decompress() writes a .tar named after the archive and registers it in an internal list that removing the file does not clear. Phar::unlinkArchive() clears both and corrupted the Zend heap when called from here, so the collision is avoided instead: untarUniqueAlias() gives the archive a uniquely named hard link — no bytes copied — and the .tar inherits that name. Both go at the end.

Parameters
$tarFile : string

Path to the tar archive file to extract.

$outputPath : string

Path to the directory where files will be extracted. The directory will be created if it does not exist.

$options : array{dryRun?: bool, keepPermissions?: bool, overwrite?: bool, maxExtractedSize?: int|null} = []

Optional flags:

  • dryRun: If true, the function does not extract files but returns the list of files that would be extracted. Default: false.
  • keepPermissions: If true, preserves the original file permissions from the archive. Default: false.
  • overwrite: If false, prevents overwriting existing files during extraction. Extraction will fail if a file already exists. Default: true.
  • maxExtractedSize: If set to a positive integer, defines the maximum total uncompressed size (in bytes) accepted during extraction. The archive is pre-scanned and a RuntimeException is thrown before any file is written if the sum of the entries' uncompressed sizes exceeds this limit. Guards against decompression-bomb attacks. Default: null (no limit).
Tags
throws
FileException

If the provided tar file is invalid or inaccessible.

DirectoryException

If the output directory cannot be created or is not writable.

RuntimeException

For extraction errors such as:

  • Path traversal attempts detected inside archive entries.
  • Attempt to overwrite existing files when overwrite is disabled.
  • Total uncompressed size exceeds maxExtractedSize (decompression-bomb protection).
  • Other errors during decompression or extraction.
Exception

Propagates unexpected exceptions during extraction wrapped as RuntimeException.

example
// Basic extraction
untar( '/path/to/archive.tar' , '/output/dir' );

// Extraction with options
untar( '/path/to/archive.tar.gz' , '/output/dir' , [
'overwrite'        => false,
'keepPermissions'  => true
]);

// Dry-run: preview contents without extracting
$files = untar( '/path/to/archive.tar' , '/output/dir' , [ 'dryRun' => true ] );
print_r( $files );

// Prevent overwriting, will throw RuntimeException if file exists
untar( '/path/to/archive.tar' , '/output/dir' , [ 'overwrite' => false ] );
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
true|array<string|int, string> —

Returns true on successful extraction, or an array of file paths (relative to archive root) if dryRun is enabled.

untarAssertExtractable()

Checks what an archive holds against what the caller allows, before anything is written.

untarAssertExtractable(array<int, array{name: string, size: int, directory: bool}> $entries, string $outputPath[, bool $overwrite = true ][, int|null $maxExtractedSize = null ][, bool $allowUnsafeLinks = false ]) : array<int, string>

The three guarantees of untar() that cannot be made while extracting — a refusal to overwrite, the decompression-bomb cap, and the dry run that reports and does nothing — all come down to the same question asked of a listing. Both engines produce that listing, so the question is asked in one place: a cap enforced one way for the system tar and another for PharData would be two caps, and one of them would be wrong.

All of it happens before the first byte is extracted. A cap that fires half way leaves a directory the caller has to clean up, and a refusal to overwrite that fires on the fourth file has already overwritten three.

Parameters
$entries : array<int, array{name: string, size: int, directory: bool}>

What the archive holds, from untarEntries() or read off a PharData iterator.

$outputPath : string

Where the archive would be extracted.

$overwrite : bool = true

When false, an existing target is refused.

$maxExtractedSize : int|null = null

Cap on the total uncompressed size; null for none.

$allowUnsafeLinks : bool = false
Tags
throws
RuntimeException

If a name escapes the output directory, a target exists while $overwrite is false, or the total exceeds the cap.

example
use function oihana\files\archive\tar\{tarBinary, untarAssertExtractable, untarEntries};

$files = untarAssertExtractable
(
    untarEntries( tarBinary() , '/backups/site.tar.gz' ) ,
    '/var/www/staging' ,
    false ,
    2 * 1024 * 1024 * 1024
) ;
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
array<int, string> —

The names of the files the archive holds, directories left out — the list a dry run returns.

untarBinaryHandles()

Whether the system `tar` can extract this archive here.

untarBinaryHandles(string $tarFile) : bool

Two conditions, and the second is the one that is easy to forget: tar does not decompress anything itself. It pipes through gzip or bzip2, so a host with ext-zlib and no gzip program has a perfectly good GNU tar that cannot open a .tar.gz. PharData, which decompresses through the PHP extensions, can — which is why the fallback exists and why this question is asked before anything is extracted.

The binary itself comes from tarBinary(), which accepts GNU tar only, having run it to ask. OIHANA_TAR_BINARY declared empty forces false here, which is the seam the tests use to run the whole extraction suite through PharData on a machine that has a usable tar.

Parameters
$tarFile : string

Path to the archive.

Tags
example
use function oihana\files\archive\tar\untarBinaryHandles;

untarBinaryHandles('/backups/site.tar.gz') ; // true where GNU tar and gzip are both present
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
bool —

True when untarWithBinary() can be used on it.

untarEntries()

Lists what an archive holds, without extracting it.

untarEntries(string $binary, string $tarFile) : array<int, array{name: string, size: int, directory: bool}>

The listing pass of untar(). It exists because three of that function's guarantees — a dry run, refusing to overwrite, and the decompression-bomb cap — all need to know the contents before the first byte is written, and tar will not tell and extract in one go. The pass streams: tar reads headers and reports, so a 2 GB archive costs a listing, not a copy.

It runs tar -tvf with three flags that are not decoration:

  • --quoting-style=literal, because the default style escapes unusual characters, so a name would come back spelled differently from the name on disk;
  • --numeric-owner, so the owner column is 501/0 and cannot contain a space, which is what lets the name be read as everything after the time;
  • LC_ALL=C, so the date column keeps the one shape this parses.

A line that does not parse raises. It would be easier to skip it, and that is exactly what must not happen: a maxExtractedSize computed from a listing with a hole in it is a cap that under-counts, which is to say a guard that no longer guards. The known case is a filename containing a newline, which no line-based listing can represent — rare, legal on POSIX, and reported rather than ignored.

Parameters
$binary : string

The GNU tar to run, from tarBinary().

$tarFile : string

Path to the archive.

Tags
throws
RuntimeException

If tar cannot read the archive, or prints a line this cannot parse.

example
use function oihana\files\archive\tar\tarBinary;
use function oihana\files\archive\tar\untarEntries;

foreach ( untarEntries( tarBinary() , '/backups/site.tar.gz' ) as $entry )
{
    printf( "%-60s %d\n" , $entry[ 'name' ] , $entry[ 'size' ] ) ;
}
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
array<int, array{name: string, size: int, directory: bool}> —

One entry per member, in archive order. A link's name stops before its -> target.

untarUniqueAlias()

Gives a compressed archive a second, uniquely named path, so decompressing it twice in one process does not collide.

untarUniqueAlias(string $tarFile, string $compression) : string|null

The collision. PharData::decompress() writes its .tar next to the archive, named after it, and registers it in a list of open phars that removing the file does not clear. Extracting the same .tar.gz a second time therefore failed with "Unable to add newly converted phar ... a phar with that name already exists" — measured on 2026-10-24, and a caller restoring several sites in one run meets it on the second.

Phar::unlinkArchive() is the documented way to clear both, and calling it from untar() corrupted the Zend heap. So the collision is avoided rather than cleaned up: give the archive a unique name and the .tar inherits it.

A hard link first, which costs no bytes and no time whatever the archive weighs; copy() only if the filesystem refuses one. Beside the archive in both cases, because a hard link cannot cross a filesystem — and because that is already where decompress() writes.

Parameters
$tarFile : string

Path to the compressed archive.

$compression : string

CompressionType::GZIP or BZIP2, as reported by tarCompressionOfFile(). The alias ends in the matching extension, so decompress() strips it the same way.

Tags
throws
UnsupportedCompressionException

If the compression has no known extension.

example
use oihana\files\enums\CompressionType;
use function oihana\files\archive\tar\untarUniqueAlias;

$alias = untarUniqueAlias( '/backups/site.tar.gz' , CompressionType::GZIP ) ;
// → '/backups/untar-6710f3c8d1e29.tar.gz', the same inode
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
string|null —

The alias, to be removed by the caller, or null when neither a link nor a copy could be made — in which case the caller is expected to carry on with the archive itself rather than fail an extraction that would otherwise work.

untarWithBinary()

Extracts an archive with the system `tar`.

untarWithBinary(string $binary, string $tarFile, string $outputPath[, bool $preservePermissions = false ]) : void

Why this exists, and it is not about speed. PharData extracts a plain .tar at a perfectly good pace — measured on 2026-10-24, 0.98 s against 0.82 s for GNU tar on the same 57 MB archive. It is the compressed case that breaks, and it breaks in three ways at once, all from one line: PharData::decompress() has PharData write a new .tar before reading it.

  1. It cannot read what this library writes. PharData's writer refuses any path component over 100 bytes — the ustar limit — so a .tar.gz holding one long name fails to extract with "filename ... is too long for tar file format", while the same archive uncompressed extracts fine. Since 1.3.0 tar() delegates to GNU tar, which stores those names without difficulty: the two halves of this library had stopped agreeing, and a stock WordPress has such a path.
  2. Memory. The rebuild costs about twice the archive — measured at ×2.26 on 60 MB and ×2.04 on 200 MB, so around 5 GB for a 2.45 GB backup.
  3. Disk. The intermediate .tar is written beside the archive, uncompressed, for the length of the extraction.

tar streams instead, and knows nothing of the 100-byte limit.

Two flags carry decisions:

  • --no-same-owner, always. As root — which is how a restore runs — GNU tar restores the owner recorded in the archive, while PharData never could. Keeping it off is what makes the two engines produce the same tree, and stops an archive from deciding who owns the files it is unpacked into.
  • -p only when asked, matching keepPermissions. Without it tar applies the umask, as PharData does.

Path traversal needs no flag: GNU tar strips a leading / and refuses any member whose name contains .., extracting nothing and exiting non-zero — which this reports.

Parameters
$binary : string

The GNU tar to run, from tarBinary().

$tarFile : string

Path to the archive.

$outputPath : string

Existing directory to extract into.

$preservePermissions : bool = false

Whether to restore the recorded permissions.

Tags
throws
RuntimeException

If tar exits non-zero.

example
use function oihana\files\archive\tar\tarBinary;
use function oihana\files\archive\tar\untarWithBinary;

untarWithBinary( tarBinary() , '/backups/site.tar.gz' , '/var/www/staging' ) ;
author

Marc Alcaraz (ekameleon)

since
1.5.0

validateTarStructure()

Validates the internal structure of a tar file.

validateTarStructure(string $filePath) : bool

This function checks whether the given file is a valid, readable tar archive. It uses the PharData class to attempt parsing the archive and iterates over a few entries to confirm structural integrity.

Note: Compressed tar files (e.g., .tar.gz, .tar.bz2) are not supported directly. Decompress them before using this function.

Parameters
$filePath : string

Path to the tar file.

Tags
example
var_dump( validateTarStructure( '/path/to/archive.tar'     ) ); // true or false
var_dump( validateTarStructure( '/path/to/invalid.tar'     ) ); // false
var_dump( validateTarStructure( '/path/to/archive.tar.gz'  ) ); // false (must decompress first)
var_dump( validateTarStructure( '/path/to/not_a_tar.txt'   ) ); // false
var_dump( validateTarStructure( '/nonexistent/file.tar'    ) ); // false
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
bool —

True if the file has a valid tar structure, false otherwise.

On this page

Search results