257 lines
7.7 KiB
PHP
257 lines
7.7 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Part of the Joomla Framework Archive Package
|
|
*
|
|
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
|
|
* @license GNU General Public License version 2 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Joomla\Archive;
|
|
|
|
use Joomla\Filesystem\File;
|
|
use Joomla\Filesystem\Folder;
|
|
use Joomla\Filesystem\Path;
|
|
|
|
/**
|
|
* Tar format adapter for the Archive package
|
|
*
|
|
* This class is inspired from and draws heavily in code and concept from the Compress package of
|
|
* The Horde Project <http://www.horde.org>
|
|
*
|
|
* @contributor Michael Slusarz <slusarz@horde.org>
|
|
* @contributor Michael Cochrane <mike@graftonhall.co.nz>
|
|
*
|
|
* @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 = [];
|
|
$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:
|
|
* <pre>
|
|
* 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
|
|
* </pre>
|
|
*
|
|
* @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',
|
|
$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;
|
|
}
|
|
}
|