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
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
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
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
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
PharDatadoes. 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é.txtbecomese´te´.txtinside 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
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
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
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.
contentorthe site root. - $limit : int|null = null
-
How many files to name at most, one at least; null names them all.
Tags
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
Return values
array{ok: bool, moved: listWhether 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> = []
-
--excludepatterns, as GNU tar reads them (tarExcludesPath()).
Tags
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
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
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
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, socachealso matchesapp/cache; - leading directory: a pattern matching a directory leaves out everything under it, so
cachematchesapp/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
--excludepatterns.
Tags
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
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
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
nullwhen 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
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
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
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
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
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
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
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
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
Return values
booltarWritableCompressions()
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
gzipwithext-zlibandbzip2withext-bz2, and nothing else —Pharknows no other. - The system
tarpipes through a program, so it offers whatever it can find:gzip,bzip2,xzandzstd. Which means a host can write.tar.zstand 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
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
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;
nullfor none. - $allowUnsafeLinks : bool = false
Tags
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
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 is501/0and 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
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, sodecompress()strips it the same way.
Tags
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.
- It cannot read what this library writes. PharData's writer refuses any path
component over 100 bytes — the
ustarlimit — so a.tar.gzholding 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.0tar()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. - 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.
- Disk. The intermediate
.taris 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.-ponly when asked, matchingkeepPermissions. Without ittarapplies 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
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
Return values
bool —True if the file has a valid tar structure, false otherwise.