barracudanetworks/ArchiveStream-php

View on GitHub
src/TarArchive.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
namespace Barracuda\ArchiveStream;

/**
 * Tar-formatted streaming archive.
 */
class TarArchive extends Archive
{
    const REGTYPE = 0;
    const DIRTYPE = 5;
    const XHDTYPE = 'x';

    /**
     * Array of specified options for the archive.
     * @var array
     */
    public $opt = array();

    /**
     * Create a new TarArchive object.
     *
     * @see \Barracuda\ArchiveStream\Archive
     */
    public function __construct()
    {
        call_user_func_array(array('parent', '__construct'), func_get_args());
        $this->opt['content_type'] = 'application/x-tar';
    }

    /**
     * Explicitly adds a directory to the tar (necessary for empty directories).
     *
     * @param  string $name Name (path) of the directory.
     * @param  array  $opt  Additional options to set ("type" will be overridden).
     * @return void
     */
    public function add_directory($name, array $opt = array())
    {
        // calculate header attributes
        $this->meth_str = 'deflate';
        $meth = 0x08;

        $opt['type'] = self::DIRTYPE;

        // send header
        $this->init_file_stream_transfer($name, $size = 0, $opt, $meth);

        // complete the file stream
        $this->complete_file_stream();
    }

    /**
     * Initialize a file stream.
     *
     * @param string $name File path or just name.
     * @param int    $size Size in bytes of the file.
     * @param array  $opt  Array containing time / type (optional).
     * @param int    $meth Method of compression to use (ignored by TarArchive class).
     * @return void
     */
    public function init_file_stream_transfer($name, $size, array $opt = array(), $meth = null)
    {
        // try to detect the type if not provided
        $type = self::REGTYPE;
        if (isset($opt['type']))
        {
            $type = $opt['type'];
        }
        elseif (substr($name, -1) == '/')
        {
            $type = self::DIRTYPE;
        }

        $dirname = dirname($name);
        $name = basename($name);

        // Remove '.' from the current directory
        $dirname = ($dirname == '.') ? '' : $dirname;

        // if we're using a container directory, prepend it to the filename
        if ($this->use_container_dir)
        {
            // container directory will end with a '/' so ensure the lower level directory name doesn't start with one
            $dirname = $this->container_dir_name . preg_replace('/^\/+/', '', $dirname);
        }

        // Remove trailing slash from directory name, because tar implies it.
        if (substr($dirname, -1) == '/')
        {
            $dirname = substr($dirname, 0, -1);
        }

        // handle long file names via PAX
        if (strlen($name) > 99 || strlen($dirname) > 154)
        {
            $pax = $this->__pax_generate(array(
                'path' => $dirname . '/' . $name
            ));

            $this->init_file_stream_transfer('', strlen($pax), array(
                'type' => self::XHDTYPE
            ));

            $this->stream_file_part($pax, $single_part = true);
            $this->complete_file_stream();
        }

        // stash the file size for later use
        $this->file_size = $size;

        // process optional arguments
        $time = isset($opt['time']) ? $opt['time'] : time();

        // build data descriptor
        $fields = array(
            array('a100', substr($name, 0, 100)),
            array('a8',   str_pad('777', 7, '0', STR_PAD_LEFT)),
            array('a8',   decoct(str_pad('0', 7, '0', STR_PAD_LEFT))),
            array('a8',   decoct(str_pad('0', 7, '0', STR_PAD_LEFT))),
            array('a12',  decoct(str_pad($size, 11, '0', STR_PAD_LEFT))),
            array('a12',  decoct(str_pad($time, 11, '0', STR_PAD_LEFT))),
            array('a8',   ''),
            array('a1',   $type),
            array('a100', ''),
            array('a6',   'ustar'),
            array('a2',   '00'),
            array('a32',  ''),
            array('a32',  ''),
            array('a8',   ''),
            array('a8',   ''),
            array('a155', substr($dirname, 0, 155)),
            array('a12',  ''),
        );

        // pack fields and calculate "total" length
        $header = $this->pack_fields($fields);

        // Compute header checksum
        $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT);
        for ($i=0; $i<6; $i++)
        {
            $header[(148 + $i)] = substr($checksum, $i, 1);
        }

        $header[154] = chr(0);
        $header[155] = chr(32);

        // print header
        $this->send($header);
    }

    /**
     * Stream the next part of the current file stream.
     *
     * @param string $data        Raw data to send.
     * @param bool   $single_part Used to determin if we can compress (not used in TarArchive class).
     * @return void
     */
    public function stream_file_part($data, $single_part = false)
    {
        // send data
        $this->send($data);

        // flush the data to the output
        flush();
    }

    /**
     * Complete the current file stream
     *
     * @return void
     */
    public function complete_file_stream()
    {
        // ensure we pad the last block so that it is 512 bytes
        $mod = ($this->file_size % 512);
        if ($mod > 0)
        {
            $this->send(pack('a' . (512 - $mod), ''));
        }

        // flush the data to the output
        flush();
    }

    /**
     * Finish an archive
     *
     * @return void
     */
    public function finish()
    {
        // adds an error log file if we've been tracking errors
        $this->add_error_log();

        // tar requires the end of the file have two 512 byte null blocks
        $this->send(pack('a1024', ''));

        // flush the data to the output
        flush();
    }

    /*******************
     * PRIVATE METHODS *
     *******************/

    /**
     * Generate unsigned checksum of header
     *
     * @param string $header File header.
     * @return string Unsigned checksum.
     * @access private
     */
    private function __computeUnsignedChecksum($header)
    {
        $unsigned_checksum = 0;

        for ($i = 0; $i < 512; $i++)
        {
            $unsigned_checksum += ord($header[$i]);
        }

        for ($i = 0; $i < 8; $i++)
        {
            $unsigned_checksum -= ord($header[148 + $i]);
        }

        $unsigned_checksum += ord(" ") * 8;

        return $unsigned_checksum;
    }

    /**
     * Generate a PAX string
     *
     * @param array $fields Key value mapping.
     * @return string PAX formated string
     * @link http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current Tar / PAX spec
     */
    private function __pax_generate(array $fields)
    {
        $lines = '';
        foreach ($fields as $name => $value)
        {
            // build the line and the size
            $line = ' ' . $name . '=' . $value . "\n";
            $size = strlen(strlen($line)) + strlen($line);

            // add the line
            $lines .= $size . $line;
        }

        return $lines;
    }
}