Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Some earlier branches remain supported and security fixes are applied to them; i

### Fixed

- Xls Writer now creates conformant CFB/DIFAT metadata at FAT-sector boundaries. [Issue #4811](https://github.com/PHPOffice/PhpSpreadsheet/issues/4811) [PR #4983](https://github.com/PHPOffice/PhpSpreadsheet/pull/4983)
- Ods Reader/Writer Drawings. [Issue #4809](https://github.com/PHPOffice/PhpSpreadsheet/issues/4809) [PR #4956](https://github.com/PHPOffice/PhpSpreadsheet/pull/4956)
- Ods Writer no longer converts cell references or commas inside string literals, so `="THIS IS E1"` is written unchanged instead of as `="THIS IS [.E1]"`. [Issue #4454](https://github.com/PHPOffice/PhpSpreadsheet/issues/4454) [PR #4962](https://github.com/PHPOffice/PhpSpreadsheet/pull/4962)
- Ods Reader/Writer slight improvement for Date styles. [PR #4960](https://github.com/PHPOffice/PhpSpreadsheet/pull/4960)
Expand Down
56 changes: 33 additions & 23 deletions src/PhpSpreadsheet/Shared/OLE/PPS/Root.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
// | Based on OLE::Storage_Lite by Kawai, Takanori |
// +----------------------------------------------------------------------+
//
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Shared\OLE;
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS;

Expand All @@ -30,14 +31,22 @@
*/
class Root extends PPS
{
private const BIG_BLOCK_SIZE = 512;

private const SMALL_BLOCK_SIZE = 64;

private const MAX_VERSION_3_STREAM_SIZE = 0x80000000;

private const MAX_REGULAR_SECTOR_COUNT = 0xFFFFFFFB;

/**
* @var resource
*/
private $fileHandle;

private ?int $smallBlockSize = null;
private int $smallBlockSize = self::SMALL_BLOCK_SIZE;

private ?int $bigBlockSize = null;
private int $bigBlockSize = self::BIG_BLOCK_SIZE;

/**
* @param null|float|int $time_1st A timestamp
Expand All @@ -64,13 +73,9 @@ public function save($fileHandle): bool
{
$this->fileHandle = $fileHandle;

// Initial Setting for saving
$this->bigBlockSize = (int) (2 ** (
(isset($this->bigBlockSize)) ? self::adjust2($this->bigBlockSize) : 9
));
$this->smallBlockSize = (int) (2 ** (
(isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6
));
// This writer implements the version-3 CFB profile only.
$this->bigBlockSize = self::BIG_BLOCK_SIZE;
$this->smallBlockSize = self::SMALL_BLOCK_SIZE;

// Make an array of PPS's (for Save)
$aList = [];
Expand All @@ -82,6 +87,7 @@ public function save($fileHandle): bool

// Make Small Data string (write SBD)
$this->_data = $this->makeSmallData($aList);
$this->assertVersion3StreamSize(strlen($this->_data));

// Write BB
$this->saveBigData((int) $iSBDcnt, $aList);
Expand Down Expand Up @@ -109,6 +115,7 @@ private function calcSize(array &$raList): array
for ($i = 0; $i < $iCount; ++$i) {
if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) {
$raList[$i]->Size = $raList[$i]->getDataLen();
$this->assertVersion3StreamSize($raList[$i]->Size);
if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) {
$iSBcnt += floor($raList[$i]->Size / $this->smallBlockSize)
+ (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0);
Expand All @@ -118,6 +125,7 @@ private function calcSize(array &$raList): array
}
}
}
$this->assertVersion3MiniStreamSize((int) $iSBcnt);
$iSmallLen = $iSBcnt * $this->smallBlockSize;
$iSlCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE);
$iSBDcnt = floor($iSBcnt / $iSlCnt) + (($iSBcnt % $iSlCnt) ? 1 : 0);
Expand All @@ -130,18 +138,16 @@ private function calcSize(array &$raList): array
return [$iSBDcnt, $iBBcnt, $iPPScnt];
}

/**
* Helper function for calculating a magic value for block sizes.
*
* @param int $i2 The argument
*
* @see save()
*/
private static function adjust2(int $i2): float
private function assertVersion3StreamSize(int $size): void
{
$iWk = log($i2) / log(2);
if ($size > self::MAX_VERSION_3_STREAM_SIZE) {
throw new Exception('OLE version-3 streams cannot exceed 2 GiB.');
}
}

return ($iWk > floor($iWk)) ? floor($iWk) + 1 : $iWk;
private function assertVersion3MiniStreamSize(int $smallBlockCount): void
{
$this->assertVersion3StreamSize($smallBlockCount * self::SMALL_BLOCK_SIZE);
}

/**
Expand All @@ -168,12 +174,16 @@ private function saveHeader(int $iSBDcnt, int $iBBcnt, int $iPPScnt): void
++$iAllW;
$iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0);
$iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0);
if ($iBdCnt <= ($iBdExL * $iBlCnt + $i1stBdL)) {
if ($iBdCnt <= ($iBdExL * ($iBlCnt - 1) + $i1stBdL)) {
break;
}
}
}

if ($iAllW + $iBdCnt > self::MAX_REGULAR_SECTOR_COUNT) {
throw new Exception('OLE version-3 output exceeds the maximum sector count.');
}

// Save Header
fwrite(
$FILE,
Expand All @@ -182,7 +192,7 @@ private function saveHeader(int $iSBDcnt, int $iBBcnt, int $iPPScnt): void
. "\x00\x00\x00\x00"
. "\x00\x00\x00\x00"
. "\x00\x00\x00\x00"
. pack('v', 0x3B)
. pack('v', 0x3E)
. pack('v', 0x03)
. pack('v', -2)
. pack('v', 9)
Expand All @@ -198,7 +208,7 @@ private function saveHeader(int $iSBDcnt, int $iBBcnt, int $iPPScnt): void
. pack('V', $iSBDcnt)
);
// Extra BDList Start, Count
if ($iBdCnt < $i1stBdL) {
if ($iBdCnt <= $i1stBdL) {
fwrite(
$FILE,
pack('V', -2) // Extra BDList Start
Expand Down Expand Up @@ -342,7 +352,7 @@ private function saveBbd(int $iSbdSize, int $iBsize, int $iPpsCnt): void
++$iAllW;
$iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0);
$iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0);
if ($iBdCnt <= ($iBdExL * $iBbCnt + $i1stBdL)) {
if ($iBdCnt <= ($iBdExL * ($iBbCnt - 1) + $i1stBdL)) {
break;
}
}
Expand Down
231 changes: 231 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/OLEPpsRootTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Shared;

use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Reader\Xls as XlsReader;
use PhpOffice\PhpSpreadsheet\Shared\OLE;
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File;
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xls as XlsWriter;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;

class OLEPpsRootTest extends TestCase
{
public function testWritesVersion3Header(): void
{
$file = tmpfile();
self::assertNotFalse($file);

try {
self::assertTrue((new Root(null, null, []))->save($file));
rewind($file);
$header = stream_get_contents($file, 512);

self::assertSame(0x003E, self::headerInteger('v', $header, 24));
self::assertSame(0x0003, self::headerInteger('v', $header, 26));
self::assertSame(9, self::headerInteger('v', $header, 30));
self::assertSame(6, self::headerInteger('v', $header, 32));
} finally {
fclose($file);
}
}

public function testKeeps109FatSectorsInTheHeaderDifat(): void
{
$stream = new File('LargeStream');
$stream->append(str_repeat('x', 13716 * 512));
$file = tmpfile();
self::assertNotFalse($file);

try {
self::assertTrue((new Root(null, null, [$stream]))->save($file));
rewind($file);
$header = stream_get_contents($file, 512);

self::assertSame(109, self::headerInteger('V', $header, 44));
self::assertSame(0xFFFFFFFE, self::headerInteger('V', $header, 68));
self::assertSame(0, self::headerInteger('V', $header, 72));
} finally {
fclose($file);
}
}

public function testCreatesDifatSectorFor110FatSectors(): void
{
$stream = new File('LargeStream');
$stream->append(str_repeat('x', 13843 * 512));
$file = tmpfile();
self::assertNotFalse($file);

try {
self::assertTrue((new Root(null, null, [$stream]))->save($file));
rewind($file);
$header = stream_get_contents($file, 512);

self::assertSame(110, self::headerInteger('V', $header, 44));
self::assertSame(13954, self::headerInteger('V', $header, 68));
self::assertSame(1, self::headerInteger('V', $header, 72));
fseek($file, (13954 + 1) * 512);
$difat = stream_get_contents($file, 512);
self::assertSame(13953, self::headerInteger('V', $difat, 0));
self::assertSame(0xFFFFFFFE, self::headerInteger('V', $difat, 508));
} finally {
fclose($file);
}
}

public function testCreatesTwoDifatSectorsFor237FatSectors(): void
{
$stream = new File('LargeStream');
$stream->append(str_repeat('x', 30096 * 512));
$file = tmpfile();
self::assertNotFalse($file);

try {
self::assertTrue((new Root(null, null, [$stream]))->save($file));
rewind($file);
$header = stream_get_contents($file, 512);

self::assertSame(237, self::headerInteger('V', $header, 44));
self::assertSame(30334, self::headerInteger('V', $header, 68));
self::assertSame(2, self::headerInteger('V', $header, 72));

fseek($file, (30334 + 1) * 512);
$firstDifat = stream_get_contents($file, 512);
self::assertSame(30206, self::headerInteger('V', $firstDifat, 0));
self::assertSame(30335, self::headerInteger('V', $firstDifat, 508));

$secondDifat = stream_get_contents($file, 512);
self::assertSame(30333, self::headerInteger('V', $secondDifat, 0));
self::assertSame(0xFFFFFFFE, self::headerInteger('V', $secondDifat, 508));
} finally {
fclose($file);
}
}

public function testRejectsStreamsLargerThanTheVersion3Limit(): void
{
$stream = new class ('TooLarge') extends File {
public function getDataLen(): int
{
return 0x80000001;
}
};
$file = tmpfile();
self::assertNotFalse($file);

try {
$this->expectException(Exception::class);
$this->expectExceptionMessage('OLE version-3 streams cannot exceed 2 GiB.');
(new Root(null, null, [$stream]))->save($file);
} finally {
fclose($file);
}
}

public function testAcceptsStreamAtTheVersion3Limit(): void
{
$method = new ReflectionMethod(Root::class, 'assertVersion3StreamSize');

self::assertNull($method->invoke(new Root(null, null, []), 0x80000000));
}

public function testRejectsOversizedAggregateMiniStreamBeforeWriting(): void
{
$method = new ReflectionMethod(Root::class, 'assertVersion3MiniStreamSize');
$root = new Root(null, null, []);

$this->expectException(Exception::class);
$this->expectExceptionMessage('OLE version-3 streams cannot exceed 2 GiB.');
$method->invoke($root, intdiv(0x80000000, 64) + 1);
}

public function testStoresStreamsAtTheMiniStreamBoundary(): void
{
$this->assertStreamStorage(4095, 0, 1);
$this->assertStreamStorage(4096, 0xFFFFFFFE, 0);
}

public function testXlsWriterProducesReadableVersion3Cfb(): void
{
$file = tmpfile();
self::assertNotFalse($file);
$metadata = stream_get_meta_data($file);
self::assertArrayHasKey('uri', $metadata);
self::assertIsString($metadata['uri']);
$path = $metadata['uri'];
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->setCellValue('A1', 'CFB v3');

try {
(new XlsWriter($spreadsheet))->save($path);
$header = file_get_contents($path, false, null, 0, 512);
self::assertIsString($header);
self::assertSame(0x003E, self::headerInteger('v', $header, 24));
self::assertSame(9, self::headerInteger('v', $header, 30));
self::assertSame(6, self::headerInteger('v', $header, 32));

$result = (new XlsReader())->load($path);
self::assertSame('CFB v3', $result->getActiveSheet()->getCell('A1')->getValue());
$result->disconnectWorksheets();
} finally {
$spreadsheet->disconnectWorksheets();
fclose($file);
}
}

private function assertStreamStorage(int $length, int $firstMiniFatSector, int $miniFatSectorCount): void
{
$data = str_repeat('x', $length);
$stream = new File('BoundaryStream');
$stream->append($data);
$file = tmpfile();
self::assertNotFalse($file);
$metadata = stream_get_meta_data($file);
self::assertArrayHasKey('uri', $metadata);
self::assertIsString($metadata['uri']);
$path = $metadata['uri'];

try {
self::assertTrue((new Root(null, null, [$stream]))->save($file));
rewind($file);
$header = stream_get_contents($file, 512);
self::assertSame($firstMiniFatSector, self::headerInteger('V', $header, 60));
self::assertSame($miniFatSectorCount, self::headerInteger('V', $header, 64));

$ole = new OLE();
$ole->read($path);
self::assertSame(2, $ole->ppsTotal());
self::assertTrue($ole->isFile(1));
self::assertSame($data, self::getDataByName($ole, 'BoundaryStream'));
} finally {
fclose($file);
}
}

private static function headerInteger(string $format, string $header, int $offset): int
{
$value = unpack($format . 'value', substr($header, $offset));
self::assertIsArray($value);
self::assertArrayHasKey('value', $value);
self::assertIsInt($value['value']);

return $value['value'];
}

private static function getDataByName(OLE $ole, string $name): string
{
foreach ($ole->_list as $index => $pps) {
if ($pps->Type === OLE::OLE_PPS_TYPE_FILE && $pps->Name === $name) {
return $ole->getData($index, 0, $pps->Size);
}
}

self::fail("OLE stream '$name' was not found.");
}
}