* * @contributor Michael Slusarz * @contributor Michael Cochrane * * @since 1.0 */ class Tar implements ExtractableInterface { /** * Tar file types. * * @var array * @since 1.0 */ private const TYPES = [ 0x0 => 'Unix file', 0x30 => 'File', 0x31 => 'Link', 0x32 => 'Symbolic link', 0x33 => 'Character special file', 0x34 => 'Block special file', 0x35 => 'Directory', 0x36 => 'FIFO special file', 0x37 => 'Contiguous file', ]; /** * Tar file data buffer * * @var string * @since 1.0 */ private $data; /** * Tar file metadata array * * @var array * @since 1.0 */ private $metadata; /** * Holds the options array. * * @var array|\ArrayAccess * @since 1.0 */ protected $options = []; /** * Create a new Archive object. * * @param array|\ArrayAccess $options An array of options or an object that implements \ArrayAccess * * @since 1.0 * @throws \InvalidArgumentException */ public function __construct($options = []) { if (!\is_array($options) && !($options instanceof \ArrayAccess)) { throw new \InvalidArgumentException( 'The options param must be an array or implement the ArrayAccess interface.' ); } $this->options = $options; } /** * Extract a ZIP compressed file to a given path * * @param string $archive Path to ZIP archive to extract * @param string $destination Path to extract archive into * * @return boolean True if successful * * @since 1.0 * @throws \RuntimeException */ public function extract($archive, $destination) { $this->metadata = null; $this->data = file_get_contents($archive); if (!$this->data) { throw new \RuntimeException('Unable to read archive'); } $this->getTarInfo($this->data); for ($i = 0, $n = \count($this->metadata); $i < $n; $i++) { $type = strtolower($this->metadata[$i]['type']); if ($type == 'file' || $type == 'unix file') { $buffer = $this->metadata[$i]['data']; $path = Path::clean($destination . '/' . $this->metadata[$i]['name']); if (!$this->isBelow($destination, $destination . '/' . $this->metadata[$i]['name'])) { throw new \OutOfBoundsException('Unable to write outside of destination path', 100); } // Make sure the destination folder exists if (!Folder::create(\dirname($path))) { throw new \RuntimeException('Unable to create destination folder ' . \dirname($path)); } if (!File::write($path, $buffer)) { throw new \RuntimeException('Unable to write entry to file ' . $path); } } } return true; } /** * Tests whether this adapter can unpack files on this computer. * * @return boolean True if supported * * @since 1.0 */ public static function isSupported() { return true; } /** * Get the list of files/data from a Tar archive buffer and builds a metadata array. * * Array structure: *
	 * KEY: Position in the array
	 * VALUES: 'attr'  --  File attributes
	 * 'data'  --  Raw file contents
	 * 'date'  --  File modification time
	 * 'name'  --  Filename
	 * 'size'  --  Original file size
	 * 'type'  --  File type
	 * 
* * @param string $data The Tar archive buffer. * * @return void * * @since 1.0 * @throws \RuntimeException */ protected function getTarInfo(&$data) { $position = 0; $returnArray = []; while ($position < \strlen($data)) { $info = @unpack( 'Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/Z8checksum/Ctypeflag/Z100link/Z6magic/Z2version/Z32uname/Z32gname/Z8devmajor/Z8devminor', substr($data, $position) ); /* * This variable has been set in the previous loop, meaning that the filename was present in the previous block * to allow more than 100 characters - see below */ if (isset($longlinkfilename)) { $info['filename'] = $longlinkfilename; unset($longlinkfilename); } if (!$info) { throw new \RuntimeException('Unable to decompress data'); } $position += 512; $contents = substr($data, $position, octdec($info['size'])); $position += ceil(octdec($info['size']) / 512) * 512; if ($info['filename']) { $file = [ 'attr' => null, 'data' => null, 'date' => octdec($info['mtime']), 'name' => trim($info['filename']), 'size' => octdec($info['size']), 'type' => self::TYPES[$info['typeflag']] ?? null, ]; if (($info['typeflag'] == 0) || ($info['typeflag'] == 0x30) || ($info['typeflag'] == 0x35)) { // File or folder. $file['data'] = $contents; $mode = hexdec(substr($info['mode'], 4, 3)); $file['attr'] = (($info['typeflag'] == 0x35) ? 'd' : '-') . (($mode & 0x400) ? 'r' : '-') . (($mode & 0x200) ? 'w' : '-') . (($mode & 0x100) ? 'x' : '-') . (($mode & 0x040) ? 'r' : '-') . (($mode & 0x020) ? 'w' : '-') . (($mode & 0x010) ? 'x' : '-') . (($mode & 0x004) ? 'r' : '-') . (($mode & 0x002) ? 'w' : '-') . (($mode & 0x001) ? 'x' : '-'); } elseif (\chr($info['typeflag']) == 'L' && $info['filename'] == '././@LongLink') { // GNU tar ././@LongLink support - the filename is actually in the contents, set a variable here so we can test in the next loop $longlinkfilename = $contents; // And the file contents are in the next block so we'll need to skip this continue; } $returnArray[] = $file; } } $this->metadata = $returnArray; } /** * Check if a path is below a given destination path * * @param string $destination The destination path * @param string $path The path to be checked * * @return boolean * * @since 2.0.1 */ private function isBelow($destination, $path): bool { $absoluteRoot = Path::clean(Path::resolve($destination)); $absolutePath = Path::clean(Path::resolve($path)); return strpos($absolutePath, $absoluteRoot) === 0; } }