indy2kro / php-iso
PHP ISO file Library. A library for reading ISO files using PHP.
Requires
- php: >=8.3
- ext-mbstring: *
- nesbot/carbon: ^3.8
Requires (Dev)
- phpstan/phpstan: ^2
- phpstan/phpstan-phpunit: ^2
- phpunit/phpunit: ^11 || ^12
- rector/rector: ^2
- squizlabs/php_codesniffer: >=3.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP Library used to read metadata and extract information from ISO files based on php-iso-file
This library follows the ISO 9660 / ECMA-119 standard.
See CHANGELOG.md for the release notes, UPGRADE-2.0.md when moving from 1.x, UPGRADE-2.1.md when moving from 2.0 and SECURITY.md to report a vulnerability (the library parses untrusted images).
Basic concepts
IsoFile- main ISO file object, contains one more descriptorsDescriptor- descriptor object which can have one of the following types defined inTypeclass:BOOT_RECORD_DESC:BootobjectPRIMARY_VOLUME_DESC:PrimaryVolumeobjectSUPPLEMENTARY_VOLUME_DESC:SupplementaryVolumeobjectPARTITION_VOLUME_DESC:PartitionobjectTERMINATOR_DESC:Terminatorobject- upon initialization of the
IsoFileobject, the descriptors will be populated automatically
- Volume descriptors contain path table inside which can be loaded using
loadTablePathTableRecord- object which contains the record information for a file/directory
- Each class contains various properties which can be used to interact with them, they are
public readonly: the parsed structures are immutable and fully initialised when the object is created, and dates areCarbonImmutable FileSystem- what can be browsed: an ISO 9660 / JolietVolumeor theUdfFileSystem. Both offerwalk,listDirectory,find,search,readFile,openStreamandcopyEntryTo, and describe files withIsoEntryobjects
Features
- Reads the ISO 9660 volume descriptors (primary, supplementary, enhanced (ISO 9660:1999), boot, partition, terminator)
- Joliet (Unicode long file names) detection and support (
IsoFile::getSupplementaryVolume()); the ISO 9660:1999 enhanced volume is available withIsoFile::getEnhancedVolume() - El Torito boot catalog parsing (
Boot::loadCatalog()), including catalogs spanning several sectors, the boot image size (BootEntry::getImageSize()) and image extraction (BootCatalog::extractImage()) - Directory tree walking with
Volume::walk()(no need to process the path table manually) and lookups by path withfind()(only the directories on the path are read),listDirectory()andsearch() - Safe extraction with
Extractor(names coming from the ISO are validated, nothing can be written outside of the destination); modification times are restored, and the optionspreserveMode(apply the Rock Ridge permissions) andcontinueOnError(collect the problems withgetErrors()instead of aborting) are available - Both-endian (M and L) path tables, directories spanning multiple sectors, multi-extent files (reported once)
- UDF file system reading (
IsoFile::getUdfFileSystem()): plain and metadata partitions (UDF 2.50, e.g. Blu-ray images), symbolic links, owner and permissions. UDF only images (and UDF bridge images carrying a stub ISO 9660 tree) are browsable throughIsoFile::getFileSystem()like ISO 9660 ones - Rock Ridge: POSIX long names, mode, owner, symbolic links, precise timestamps (
TF), device numbers (PN), continuation areas and relocated directories (Volume::walk(),IsoEntry::$rockRidge); symbolic links are never created on extraction IsoEntryexposesuid,gid,modeandsymlinkTarget(Rock Ridge and UDF)- Reading file content:
find(),search(),readFile()andopenStream(); streams are seekable and read the image on demand (Util\EntryStream), nothing is copied to a temporary file - Images coming from non-seekable streams (standard input, pipes) with
IsoFile::fromStream()
Known limitations
- UDF: only 2048 bytes blocks are read; sparable and virtual (VAT) partitions are reported as unsupported. For metadata partitions the mirror and bitmap files are ignored
Installation
This class can easily be installed via Composer:
composer require indy2kro/php-iso
Requires PHP 8.3 or newer and nesbot/carbon.
CLI tool
This tool also provides a CLI tool that can be used to view information about ISO files - bin\isotool:
Description:
Tool to process ISO files
Usage:
isotool [options] --file=<path>
Options:
-f, --file=<path> Path for the ISO file, "-" reads it from the standard input (mandatory)
-l, --list Print only the list of files (path and size)
-j, --json Print all the information as JSON
--ndjson With --list: one JSON object per line and entry, streamed (newline delimited JSON)
-x, --extract=<extract_path> Extract files in the given location
-c, --cat=<path> Write the content of a file of the ISO to the standard output
--find=<pattern> List the files matching a pattern (e.g. "*.txt", case insensitive)
--extract-boot=<path> Write the El Torito default boot image to the given file
--files Also list the files of every volume in the default information output
--volume=<name> File system used by --list, --cat, --find and --extract: primary, joliet or udf
(default: Joliet, else primary, else UDF)
--no-rock-ridge Ignore the Rock Ridge extensions (use the plain ISO 9660 / Joliet names)
--strict Fail on the first incomplete listing (depth limit, unreadable or corrupt directory)
instead of printing "WARNING: ..." lines on the standard error output
-h, --help Show this help
Only one of --list, --json, --extract, --cat, --find and --extract-boot can be used at a time.
Flags can be bundled (e.g. -lj).
Exit codes:
0 success
1 usage error (unknown, conflicting or missing options, including a missing --file)
2 invalid file argument
3 the ISO could not be read or extracted
Sample usage (isotool -f fixtures/1mb.iso --files, without --files the lists of files are left out):
Input ISO file: fixtures/1mb.iso
Number of descriptors: 3
- Primary volume descriptor
- System ID: Win32
- Volume ID: 25_12_2024
- App ID: PowerISO
- File Structure Version: 1
- Volume Space Size: 542
- Volume Set Size: 1
- Volume SeqNum: 1
- Block size: 2048
- Volume Set ID:
- Publisher ID:
- Preparer ID:
- Copyright File ID:
- Abstract File ID:
- Bibliographic File ID:
- Creation Date: 2024-12-25 14:01:20
- Modification Date: 2024-12-25 14:01:20
- Expiration Date:
- Effective Date:
- Files:
/1MB.PNG (location: 30) (length: 1048576)
- Supplementary volume descriptor
- System ID: Win32
- Volume ID: 25_12_2024
- App ID: PowerISO
- File Structure Version: 1
- Volume Space Size: 542
- Volume Set Size: 1
- Volume SeqNum: 1
- Block size: 2048
- Volume Set ID:
- Publisher ID:
- Preparer ID:
- Copyright File ID: ?
- Abstract File ID: ?
- Bibliographic File ID: ?
- Creation Date: 2024-12-25 14:01:20
- Modification Date: 2024-12-25 14:01:20
- Expiration Date:
- Effective Date:
- Joliet Level: 3
- Files:
/1mb.png (location: 30) (length: 1048576)
- Terminator descriptor
Other examples:
isotool -f image.iso --list # path and size of every file (symbolic links show "-> target")
isotool -f image.iso --find "*.txt" # search by name (case insensitive); a pattern with "/" matches the path
isotool -f image.iso --cat /DIR/FILE.TXT > file.txt
isotool -f image.iso --volume=primary -l # the ISO 9660 tree instead of the Joliet one
isotool -f image.iso --extract-boot boot.img
isotool -f image.iso -l --ndjson # one JSON object per line, streamed
isotool -f image.iso --json | jq .
cat image.iso | isotool -f - -l # standard input
Usage
Walking the files of an ISO (Joliet names are used when present):
<?php use PhpIso\IsoFile; $isoFile = new IsoFile('test.iso'); $volume = $isoFile->getPreferredVolume(); foreach ($volume->walk($isoFile) as $entry) { echo $entry->path, $entry->isDirectory ? '/' : ' (' . $entry->size . ' bytes)', PHP_EOL; }
getFileSystem() or getPreferredVolume()? getPreferredVolume() returns the Joliet volume (else the primary one), it is only about ISO 9660 and returns null for UDF only images. getFileSystem() returns a FileSystem that also covers UDF: the preferred volume, or the UDF file system for UDF only images and for bridge images (e.g. Windows install media) whose ISO 9660 tree is only a stub. Prefer getFileSystem() unless you need a Volume (path table, descriptor fields).
$fileSystem = $isoFile->getFileSystem(); // ?FileSystem
Looking up and reading files (find() matches the exact name first, then ignoring the case):
$entry = $fileSystem->find($isoFile, '/dir/readme.txt'); if ($entry !== null) { $content = $fileSystem->readFile($isoFile, $entry); // whole file, limited by IsoFile::MAX_READ_LENGTH $stream = $fileSystem->openStream($isoFile, $entry); // seekable stream, read from the image on demand echo fread($stream, 100); fclose($stream); } foreach ($fileSystem->search($isoFile, '*.txt') as $match) { echo $match->path, PHP_EOL; }
Entries carry the Rock Ridge / UDF attributes:
echo $entry->mode, $entry->uid, $entry->gid; // null when the image has no such information echo $entry->getSymlinkTarget(); // null unless the entry is a symbolic link
Reading an image from a pipe or standard input (copied to a temporary file, removed when the object is destroyed; at most IsoFile::MAX_STREAM_BYTES by default):
$isoFile = IsoFile::fromStream(STDIN); $isoFile = IsoFile::fromStream($stream, 512 * 1024 * 1024); // custom limit in bytes
Extracting everything (names are untrusted input, the extractor refuses names that could escape the destination):
(new \PhpIso\Extractor())->extract($isoFile, $fileSystem, '/tmp/out'); // keep the Rock Ridge permissions and carry on after an unsafe name or a failed write $extractor = new \PhpIso\Extractor(preserveMode: true, continueOnError: true); $extractor->extract($isoFile, $fileSystem, '/tmp/out'); print_r($extractor->getErrors()); // entry path => message
Reading the El Torito boot catalog and extracting the boot image:
$catalog = $isoFile->getBootRecord()?->loadCatalog($isoFile); $entry = $catalog?->getDefaultEntry(); echo $entry?->getMediaName(), PHP_EOL; $catalog?->extractImage($isoFile, $entry, 'boot.img');
Errors and exceptions
Every failure caused by the image or by an invalid argument is a PhpIso\Exception (PhpIso\Descriptor\Exception extends it), nothing else should escape for a damaged image; catching PhpIso\Exception is enough:
new IsoFile($path): the file does not exist, is not a regular file or cannot be opened, the volume descriptors are truncated or invalid, or there are more thanIsoFile::MAX_DESCRIPTORSof themIsoFile::fromStream(): the stream cannot be read, is bigger than the limit, or is not an image (same as above)IsoFile::getUdfFileSystem()andgetFileSystem(): the UDF structures are present but unsupported or corrupt (getFileSystem()falls back to the ISO 9660 tree when it has one)FileSystem::readFile(),openStream(),copyEntryTo(): the entry is a directory, the file is bigger than$maxSize, or its data lies outside of the imageExtractor::extract(): unsafe names (path traversal, Windows reserved names...), failed writes (collected ingetErrors()instead whencontinueOnErroris set)Volume::loadTable(),Boot::loadCatalog(),BootCatalog::extractImage(): corrupt tables or catalogs
Limits
The image is untrusted input, so sizes taken from it are bounded:
IsoFile::MAX_READ_LENGTH(64 MiB): the largest single read, so a directory, path table orreadFile()result cannot be biggerIsoFile::MAX_STREAM_BYTES(4 GiB): the largest stream accepted byIsoFile::fromStream()unless another limit is givenIsoFile::MAX_DESCRIPTORS(64): the maximum number of volume descriptors readwalk($isoFile, $maxDepth = 64): directories nested deeper are not listed A listing can therefore be incomplete (too deep or oversized directories, corrupt directory records, unreadable UDF entries). Pass aPhpIso\WalkWarningscollector towalk(),listDirectory(),find(),search()orExtractor::extract()to find out (new WalkWarnings(strict: true)throws aPhpIso\Exceptionat the first problem instead):
$warnings = new \PhpIso\WalkWarnings(); foreach ($volume->walk($isoFile, 64, $warnings) as $entry) { // ... } foreach ($warnings->all() as $warning) { echo 'WARNING: ' . $warning . PHP_EOL; }
Low level access to the descriptors and the path table:
<?php use PhpIso\IsoFile; use PhpIso\Descriptor\Type; use PhpIso\Descriptor\PrimaryVolume; use PhpIso\FileDirectory; use PhpIso\PathTableRecord; $isoFilePath = 'test.iso'; $isoFile = new IsoFile($isoFilePath); // you can process each descriptor using $isoFile->descriptors directly /** @var PrimaryVolume $primaryVolumeDescriptor */ $primaryVolumeDescriptor = $isoFile->descriptors[Type::PRIMARY_VOLUME_DESC]; // get the path table $pathTable = $primaryVolumeDescriptor->loadTable($isoFile); $paths = []; /** @var PathTableRecord $pathRecord */ foreach ($pathTable as $pathRecord) { $currentPath = $pathRecord->getFullPath($pathTable); $paths[$currentPath] = []; // check extents $extents = $pathRecord->loadExtents($isoFile, $primaryVolumeDescriptor->blockSize); if ($extents !== false) { /** @var FileDirectory $extentRecord */ foreach ($extents as $extentRecord) { $path = $extentRecord->fileId; if ($extentRecord->isDirectory()) { $path .= '/'; } $paths[$currentPath][] = $path; } } } print_r($paths);