MAXakaWIZARD/xls-writer

View on GitHub
src/OLE/PpsRoot.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Xls\OLE;

class PpsRoot extends PPS
{
    /**
     * @var
     */
    protected $smallBlockSize;

    /**
     * @var
     */
    protected $bigBlockSize;

    /**
     * @var
     */
    protected $rootFilePointer;

    /**
     * @param integer $timestamp A timestamp
     * @param PpsFile[] $children
     */
    public function __construct(
        $timestamp = null,
        $children = array()
    ) {
        parent::__construct(
            null,
            OLE::asc2Ucs('Root Entry'),
            self::PPS_TYPE_ROOT,
            null,
            null,
            null,
            $timestamp,
            null,
            $children
        );
    }

    /**
     * Method for saving the whole OLE container (including files).
     *
     * @param string $filename The name of the file where to save the OLE container
     * @throws \Exception
     * @return boolean
     */
    public function save($filename)
    {
        $this->openFile($filename);

        $this->setBlockSizes();

        // Make an array of PPS's (for Save)
        $list = array();
        self::setPointers($list, array($this));

        list($iSBDcnt, $iBBcnt, $iPPScnt) = $this->calcSize($list);

        $this->saveHeader($iSBDcnt, $iBBcnt, $iPPScnt);
        $this->saveSmallData($list);
        $this->saveBigData($iSBDcnt, $list);
        $this->savePps($list);
        $this->saveBigBlockChain($iSBDcnt, $iBBcnt, $iPPScnt);

        fclose($this->rootFilePointer);
    }

    /**
     *
     */
    protected function setBlockSizes()
    {
        $this->bigBlockSize = pow(2, 9);
        $this->smallBlockSize = pow(2, 6);
    }

    /**
     * @param $filename
     *
     * @throws \Exception
     */
    protected function openFile($filename)
    {
        $this->rootFilePointer = @fopen($filename, "wb");
        if ($this->rootFilePointer === false) {
            throw new \Exception("Can't open $filename. It may be in use or protected.");
        }
    }

    /**
     * @param $size
     * @param $blockSize
     *
     * @return float
     */
    protected function getBlocksCount($size, $blockSize)
    {
        return floor($size / $blockSize) + (($size % $blockSize) ? 1 : 0);
    }

    /**
     * Calculate some numbers
     *
     * @param PPS[] $list Reference to an array of PPS's
     *
     * @return array The array of numbers
     */
    protected function calcSize($list)
    {
        $iSBcnt = 0;
        $iBBcnt = 0;
        foreach ($list as $item) {
            if (!$item->isFile()) {
                continue;
            }

            $size = $item->getSize();
            if ($size < self::DATA_SIZE_SMALL) {
                $iSBcnt += $this->getBlocksCount($size, $this->smallBlockSize);
            } else {
                $iBBcnt += $this->getBlocksCount($size, $this->bigBlockSize);
            }
        }

        $iSmallLen = $iSBcnt * $this->smallBlockSize;
        $iSlCnt = $this->getPointersPerBlock($this->bigBlockSize);
        $iSBDcnt = $this->getBlocksCount($iSBcnt, $iSlCnt);
        $iBBcnt += $this->getBlocksCount($iSmallLen, $this->bigBlockSize);
        $iBdCnt = $this->getPointersPerBlock($this->bigBlockSize, self::PPS_SIZE);
        $iPPScnt = $this->getBlocksCount(count($list), $iBdCnt);

        return array($iSBDcnt, $iBBcnt, $iPPScnt);
    }

    /**
     * Saving big data (PPS's with data bigger than OLE_DATA_SIZE_SMALL)
     *
     * @param integer $iStBlk
     * @param PPS[] &$list Reference to array of PPS's
     */
    public function saveBigData($iStBlk, &$list)
    {
        foreach ($list as $item) {
            $size = $item->getSize();
            if ($size >= self::DATA_SIZE_SMALL
                || ($item->isRoot() && $item->hasData())
            ) {
                $this->write($item->getData());

                if ($size % $this->bigBlockSize) {
                    $zeroByteCount = ($this->bigBlockSize - ($size % $this->bigBlockSize));
                    $this->write(str_repeat("\x00", $zeroByteCount));
                }

                // Set For PPS
                $item->setStartBlock($iStBlk);
                $iStBlk += $this->getBlocksCount($size, $this->bigBlockSize);
            }
        }
    }

    /**
     * get small data (PPS's with data smaller than OLE_DATA_SIZE_SMALL)
     *
     * @param PPS[] &$list Reference to array of PPS's
     * @return string
     */
    protected function saveSmallData(&$list)
    {
        $result = '';
        $iSmBlk = 0;
        foreach ($list as $item) {
            if (!$item->isFile()) {
                continue;
            }

            $size = $item->getSize();
            if ($size <= 0 || $size >= self::DATA_SIZE_SMALL) {
                continue;
            }

            $sbCount = $this->getBlocksCount($size, $this->smallBlockSize);
            // Add to SBD
            for ($j = 0; $j < $sbCount - 1; $j++) {
                $this->writeUlong($j + $iSmBlk + 1);
            }
            $this->writeUlong(-2);

            $result .= $item->getData();

            $exp = $size % $this->smallBlockSize;
            if ($exp) {
                $zeroByteCount = $this->smallBlockSize - $exp;
                $result .= str_repeat("\x00", $zeroByteCount);
            }

            $item->setStartBlock($iSmBlk);
            $iSmBlk += $sbCount;
        }

        $sbCount = $this->getPointersPerBlock($this->bigBlockSize);
        if ($iSmBlk % $sbCount) {
            $repeatCount = $sbCount - ($iSmBlk % $sbCount);
            $this->writeUlong(-1, $repeatCount);
        }

        $this->data = $result;
    }

    /**
     * Saves all the PPS's WKs
     *
     * @param PPS[] $list Reference to an array with all PPS's
     */
    protected function savePps(&$list)
    {
        // Save each PPS WK
        $raListCount = count($list);
        for ($i = 0; $i < $raListCount; $i++) {
            $this->write($list[$i]->getPpsWk());
        }

        // Adjust for Block
        $iCnt = count($list);
        $iBCnt = $this->getPointersPerBlock($this->bigBlockSize, self::PPS_SIZE);
        if ($iCnt % $iBCnt) {
            $zeroByteCount = ($iBCnt - ($iCnt % $iBCnt)) * self::PPS_SIZE;
            $this->write(str_repeat("\x00", $zeroByteCount));
        }
    }

    /**
     * @param     $value
     * @param int $count
     */
    protected function writeUlong($value, $count = 1)
    {
        $packed = pack("V", $value);
        $this->write(str_repeat($packed, $count));
    }

    /**
     * @param $data
     */
    protected function write($data)
    {
        fwrite($this->rootFilePointer, $data);
    }

    /**
     * Saving Big Block Depot
     *
     * @param integer $numSbBlocks - number of Smallblock depot blocks
     * @param integer $numBbBlocks - number of Bigblock depot blocks
     * @param integer $numPpsBlocks - number of PropertySetStorage blocks
     */
    protected function saveBigBlockChain($numSbBlocks, $numBbBlocks, $numPpsBlocks)
    {
        $info = $this->calcBigBlockChain($numSbBlocks, $numBbBlocks, $numPpsBlocks);
        $headerEntriesCount = $info["header_list_entries"];
        $entriesCount = $info["list_entries"];
        $entriesPerBlock = $info["entries_per_block"];

        if ($numSbBlocks > 0) {
            for ($i = 0; $i < $numSbBlocks - 1; $i++) {
                $this->writeUlong($i + 1);
            }
            $this->writeUlong(-2);
        }

        for ($i = 0; $i < $numBbBlocks - 1; $i++) {
            $this->writeUlong($i + $numSbBlocks + 1);
        }
        $this->writeUlong(-2);

        for ($i = 0; $i < $numPpsBlocks - 1; $i++) {
            $this->writeUlong($i + $numSbBlocks + $numBbBlocks + 1);
        }
        $this->writeUlong(-2);

        $this->writeUlong(0xFFFFFFFD, $info["FD_entries"]);
        $this->writeUlong(0xFFFFFFFC, $info["FC_entries"]);

        // Adjust for Block
        $allEntries = $numSbBlocks
            + $numBbBlocks
            + $numPpsBlocks
            + $info["FD_entries"]
            + $info["FC_entries"];
        if ($allEntries % $entriesPerBlock) {
            $rest = $entriesPerBlock - ($allEntries % $entriesPerBlock);
            $this->writeUlong(-1, $rest);
        }

        // Extra BDList
        if ($entriesCount > $headerEntriesCount) {
            $iN = 0;
            $iNb = 0;
            $lastEntryIdx = $entriesPerBlock - 1;

            for ($i = $headerEntriesCount; $i < $entriesCount; $i++, $iN++) {
                if ($iN >= $lastEntryIdx) {
                    $iN = 0;
                    $iNb++;

                    $val = $numSbBlocks
                        + $numBbBlocks
                        + $numPpsBlocks
                        + $info["FD_entries"]
                        + $iNb;
                    $this->writeUlong($val);
                }

                $this->writeUlong($numBbBlocks + $numSbBlocks + $numPpsBlocks + $i);
            }

            $allEntries = $entriesCount - $headerEntriesCount;
            if ($allEntries % $lastEntryIdx) {
                $rest = $lastEntryIdx - ($allEntries % $lastEntryIdx);
                $this->writeUlong(-1, $rest);
            }

            $this->writeUlong(-2);
        }
    }

    /**
     * Save OLE header
     *
     * @param integer $numSbBlocks - number of Smallblock depot blocks
     * @param integer $numBbBlocks - number of Bigblock depot blocks
     * @param integer $numPpsBlocks - number of PropertySetStorage blocks
     */
    public function saveHeader($numSbBlocks, $numBbBlocks, $numPpsBlocks)
    {
        $info = $this->calcBigBlockChain($numSbBlocks, $numBbBlocks, $numPpsBlocks);
        $headerEntriesCount = $info["header_list_entries"];
        $entriesCount = $info["list_entries"];

        $this->write(
            "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"
            . pack("V4", 0, 0, 0, 0)
            . pack("v6", 0x3b, 0x03, -2, 9, 6, 0)
            . pack("V2", 0, 0)
            . pack("V", $entriesCount)
            . pack("V", $numSbBlocks + $numBbBlocks) //ROOT START
            . pack("V2", 0, 0x1000)
        );

        //Small Block Depot
        $value = ($numSbBlocks > 0) ? 0 : -2;
        $this->writeUlong($value);
        $this->writeUlong($numSbBlocks);

        // Extra BDList Start, Count
        if ($entriesCount < $headerEntriesCount) {
            $this->writeUlong(-2); // Extra BDList Start
            $this->writeUlong(0); // Extra BDList Count
        } else {
            $this->writeUlong($numSbBlocks + $numBbBlocks + $numPpsBlocks + $info["FD_entries"]);
            $this->writeUlong($info["FC_entries"]);
        }

        // BDList
        for ($i = 0; $i < $headerEntriesCount && $i < $entriesCount; $i++) {
            $this->writeUlong($numBbBlocks + $numSbBlocks + $numPpsBlocks + $i);
        }

        if ($i < $headerEntriesCount) {
            $repeatCount = $headerEntriesCount - $i;
            $this->writeUlong(-1, $repeatCount);
        }
    }

    /**
     * New method to calculate Bigblock chain
     *
     * @param integer $numSb - number of Smallblock depot blocks
     * @param integer $numBb - number of Bigblock depot blocks
     * @param integer $numPps - number of PropertySetStorage blocks
     * @return array
     */
    protected function calcBigBlockChain($numSb, $numBb, $numPps)
    {
        $totalBlocks = $numSb + $numBb + $numPps;
        $info = array(
            "entries_per_block" => $this->getPointersPerBlock($this->bigBlockSize),
            "header_list_entries" => $this->getPointersPerBlock($this->bigBlockSize - 0x4C),
            "entries" => $totalBlocks,
            "ext_list_entries" => 0,
            "FC_entries" => 0,
            "FD_entries" => $this->getNumberOfPointerBlocks($totalBlocks)
        );

        $info["list_entries"] = $this->getNumberOfPointerBlocks(
            $totalBlocks + $info["FD_entries"]
        );

        if ($info["list_entries"] <= $info["header_list_entries"]) {
            return $info;
        }

        return $this->calcBigBlockChainExtra($info);
    }

    /**
     * @param array $info
     *
     * @return array
     */
    protected function calcBigBlockChainExtra($info)
    {
        while (true) {
            $pointerBlocksCount = $this->getNumberOfPointerBlocks(
                $info["entries"] + $info["FD_entries"] + $info["FC_entries"]
            );

            if ($info["list_entries"] >= $pointerBlocksCount) {
                break;
            }

            $info["list_entries"] = $pointerBlocksCount;
            $info["ext_list_entries"] = $info["list_entries"] - $info["header_list_entries"];
            $info["FC_entries"] = $this->getNumberOfPointerBlocks(
                $info["ext_list_entries"]
            );
            $info["FD_entries"] = $this->getNumberOfPointerBlocks(
                $info["entries"] + $info["FD_entries"] + $info["FC_entries"]
            );
        }

        return $info;
    }

    /**
     * Calculates number of pointer blocks
     *
     * @param integer $numPointers - number of pointers
     *
     * @return int
     */
    protected function getNumberOfPointerBlocks($numPointers)
    {
        return $this->getBlocksCount($numPointers, $this->getPointersPerBlock($this->bigBlockSize));
    }

    /**
     * @param int $blockSize
     * @param int $pointerSize
     *
     * @return int
     */
    protected function getPointersPerBlock($blockSize, $pointerSize = self::LONG_INT_SIZE)
    {
        return intval(floor($blockSize / $pointerSize));
    }
}