| 1 |
<?php |
| 2 |
|
| 3 |
namespace PhpOffice\PhpSpreadsheet\Shared; |
| 4 |
|
| 5 |
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; |
| 6 |
|
| 7 |
class OLERead |
| 8 |
{ |
| 9 |
private $data = ''; |
| 10 |
|
| 11 |
// Size of a sector = 512 bytes |
| 12 |
const BIG_BLOCK_SIZE = 0x200; |
| 13 |
|
| 14 |
// Size of a short sector = 64 bytes |
| 15 |
const SMALL_BLOCK_SIZE = 0x40; |
| 16 |
|
| 17 |
// Size of a directory entry always = 128 bytes |
| 18 |
const PROPERTY_STORAGE_BLOCK_SIZE = 0x80; |
| 19 |
|
| 20 |
// Minimum size of a standard stream = 4096 bytes, streams smaller than this are stored as short streams |
| 21 |
const SMALL_BLOCK_THRESHOLD = 0x1000; |
| 22 |
|
| 23 |
// header offsets |
| 24 |
const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS = 0x2c; |
| 25 |
const ROOT_START_BLOCK_POS = 0x30; |
| 26 |
const SMALL_BLOCK_DEPOT_BLOCK_POS = 0x3c; |
| 27 |
const EXTENSION_BLOCK_POS = 0x44; |
| 28 |
const NUM_EXTENSION_BLOCK_POS = 0x48; |
| 29 |
const BIG_BLOCK_DEPOT_BLOCKS_POS = 0x4c; |
| 30 |
|
| 31 |
// property storage offsets (directory offsets) |
| 32 |
const SIZE_OF_NAME_POS = 0x40; |
| 33 |
const TYPE_POS = 0x42; |
| 34 |
const START_BLOCK_POS = 0x74; |
| 35 |
const SIZE_POS = 0x78; |
| 36 |
|
| 37 |
public $wrkbook; |
| 38 |
|
| 39 |
public $summaryInformation; |
| 40 |
|
| 41 |
public $documentSummaryInformation; |
| 42 |
|
| 43 |
/** |
| 44 |
* @var int |
| 45 |
*/ |
| 46 |
private $numBigBlockDepotBlocks; |
| 47 |
|
| 48 |
/** |
| 49 |
* @var int |
| 50 |
*/ |
| 51 |
private $rootStartBlock; |
| 52 |
|
| 53 |
/** |
| 54 |
* @var int |
| 55 |
*/ |
| 56 |
private $sbdStartBlock; |
| 57 |
|
| 58 |
/** |
| 59 |
* @var int |
| 60 |
*/ |
| 61 |
private $extensionBlock; |
| 62 |
|
| 63 |
/** |
| 64 |
* @var int |
| 65 |
*/ |
| 66 |
private $numExtensionBlocks; |
| 67 |
|
| 68 |
/** |
| 69 |
* @var string |
| 70 |
*/ |
| 71 |
private $bigBlockChain; |
| 72 |
|
| 73 |
/** |
| 74 |
* @var string |
| 75 |
*/ |
| 76 |
private $smallBlockChain; |
| 77 |
|
| 78 |
/** |
| 79 |
* @var string |
| 80 |
*/ |
| 81 |
private $entry; |
| 82 |
|
| 83 |
/** |
| 84 |
* @var int |
| 85 |
*/ |
| 86 |
private $rootentry; |
| 87 |
|
| 88 |
/** |
| 89 |
* @var array |
| 90 |
*/ |
| 91 |
private $props = []; |
| 92 |
|
| 93 |
/** |
| 94 |
* Read the file. |
| 95 |
* |
| 96 |
* @param $pFilename string Filename |
| 97 |
* |
| 98 |
* @throws ReaderException |
| 99 |
*/ |
| 100 |
public function read($pFilename) |
| 101 |
{ |
| 102 |
File::assertFile($pFilename); |
| 103 |
|
| 104 |
// Get the file identifier |
| 105 |
// Don't bother reading the whole file until we know it's a valid OLE file |
| 106 |
$this->data = file_get_contents($pFilename, false, null, 0, 8); |
| 107 |
|
| 108 |
// Check OLE identifier |
| 109 |
$identifierOle = pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1); |
| 110 |
if ($this->data != $identifierOle) { |
| 111 |
throw new ReaderException('The filename ' . $pFilename . ' is not recognised as an OLE file'); |
| 112 |
} |
| 113 |
|
| 114 |
// Get the file data |
| 115 |
$this->data = file_get_contents($pFilename); |
| 116 |
|
| 117 |
// Total number of sectors used for the SAT |
| 118 |
$this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS); |
| 119 |
|
| 120 |
// SecID of the first sector of the directory stream |
| 121 |
$this->rootStartBlock = self::getInt4d($this->data, self::ROOT_START_BLOCK_POS); |
| 122 |
|
| 123 |
// SecID of the first sector of the SSAT (or -2 if not extant) |
| 124 |
$this->sbdStartBlock = self::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS); |
| 125 |
|
| 126 |
// SecID of the first sector of the MSAT (or -2 if no additional sectors are used) |
| 127 |
$this->extensionBlock = self::getInt4d($this->data, self::EXTENSION_BLOCK_POS); |
| 128 |
|
| 129 |
// Total number of sectors used by MSAT |
| 130 |
$this->numExtensionBlocks = self::getInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS); |
| 131 |
|
| 132 |
$bigBlockDepotBlocks = []; |
| 133 |
$pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS; |
| 134 |
|
| 135 |
$bbdBlocks = $this->numBigBlockDepotBlocks; |
| 136 |
|
| 137 |
if ($this->numExtensionBlocks != 0) { |
| 138 |
$bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4; |
| 139 |
} |
| 140 |
|
| 141 |
for ($i = 0; $i < $bbdBlocks; ++$i) { |
| 142 |
$bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); |
| 143 |
$pos += 4; |
| 144 |
} |
| 145 |
|
| 146 |
for ($j = 0; $j < $this->numExtensionBlocks; ++$j) { |
| 147 |
$pos = ($this->extensionBlock + 1) * self::BIG_BLOCK_SIZE; |
| 148 |
$blocksToRead = min($this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1); |
| 149 |
|
| 150 |
for ($i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; ++$i) { |
| 151 |
$bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); |
| 152 |
$pos += 4; |
| 153 |
} |
| 154 |
|
| 155 |
$bbdBlocks += $blocksToRead; |
| 156 |
if ($bbdBlocks < $this->numBigBlockDepotBlocks) { |
| 157 |
$this->extensionBlock = self::getInt4d($this->data, $pos); |
| 158 |
} |
| 159 |
} |
| 160 |
|
| 161 |
$pos = 0; |
| 162 |
$this->bigBlockChain = ''; |
| 163 |
$bbs = self::BIG_BLOCK_SIZE / 4; |
| 164 |
for ($i = 0; $i < $this->numBigBlockDepotBlocks; ++$i) { |
| 165 |
$pos = ($bigBlockDepotBlocks[$i] + 1) * self::BIG_BLOCK_SIZE; |
| 166 |
|
| 167 |
$this->bigBlockChain .= substr($this->data, $pos, 4 * $bbs); |
| 168 |
$pos += 4 * $bbs; |
| 169 |
} |
| 170 |
|
| 171 |
$pos = 0; |
| 172 |
$sbdBlock = $this->sbdStartBlock; |
| 173 |
$this->smallBlockChain = ''; |
| 174 |
while ($sbdBlock != -2) { |
| 175 |
$pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; |
| 176 |
|
| 177 |
$this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); |
| 178 |
$pos += 4 * $bbs; |
| 179 |
|
| 180 |
$sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4); |
| 181 |
} |
| 182 |
|
| 183 |
// read the directory stream |
| 184 |
$block = $this->rootStartBlock; |
| 185 |
$this->entry = $this->_readData($block); |
| 186 |
|
| 187 |
$this->readPropertySets(); |
| 188 |
} |
| 189 |
|
| 190 |
/** |
| 191 |
* Extract binary stream data. |
| 192 |
* |
| 193 |
* @param int $stream |
| 194 |
* |
| 195 |
* @return string |
| 196 |
*/ |
| 197 |
public function getStream($stream) |
| 198 |
{ |
| 199 |
if ($stream === null) { |
| 200 |
return null; |
| 201 |
} |
| 202 |
|
| 203 |
$streamData = ''; |
| 204 |
|
| 205 |
if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) { |
| 206 |
$rootdata = $this->_readData($this->props[$this->rootentry]['startBlock']); |
| 207 |
|
| 208 |
$block = $this->props[$stream]['startBlock']; |
| 209 |
|
| 210 |
while ($block != -2) { |
| 211 |
$pos = $block * self::SMALL_BLOCK_SIZE; |
| 212 |
$streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE); |
| 213 |
|
| 214 |
$block = self::getInt4d($this->smallBlockChain, $block * 4); |
| 215 |
} |
| 216 |
|
| 217 |
return $streamData; |
| 218 |
} |
| 219 |
$numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE; |
| 220 |
if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) { |
| 221 |
++$numBlocks; |
| 222 |
} |
| 223 |
|
| 224 |
if ($numBlocks == 0) { |
| 225 |
return ''; |
| 226 |
} |
| 227 |
|
| 228 |
$block = $this->props[$stream]['startBlock']; |
| 229 |
|
| 230 |
while ($block != -2) { |
| 231 |
$pos = ($block + 1) * self::BIG_BLOCK_SIZE; |
| 232 |
$streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); |
| 233 |
$block = self::getInt4d($this->bigBlockChain, $block * 4); |
| 234 |
} |
| 235 |
|
| 236 |
return $streamData; |
| 237 |
} |
| 238 |
|
| 239 |
/** |
| 240 |
* Read a standard stream (by joining sectors using information from SAT). |
| 241 |
* |
| 242 |
* @param int $bl Sector ID where the stream starts |
| 243 |
* |
| 244 |
* @return string Data for standard stream |
| 245 |
*/ |
| 246 |
private function _readData($bl) |
| 247 |
{ |
| 248 |
$block = $bl; |
| 249 |
$data = ''; |
| 250 |
|
| 251 |
while ($block != -2) { |
| 252 |
$pos = ($block + 1) * self::BIG_BLOCK_SIZE; |
| 253 |
$data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); |
| 254 |
$block = self::getInt4d($this->bigBlockChain, $block * 4); |
| 255 |
} |
| 256 |
|
| 257 |
return $data; |
| 258 |
} |
| 259 |
|
| 260 |
/** |
| 261 |
* Read entries in the directory stream. |
| 262 |
*/ |
| 263 |
private function readPropertySets() |
| 264 |
{ |
| 265 |
$offset = 0; |
| 266 |
|
| 267 |
// loop through entires, each entry is 128 bytes |
| 268 |
$entryLen = strlen($this->entry); |
| 269 |
while ($offset < $entryLen) { |
| 270 |
// entry data (128 bytes) |
| 271 |
$d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE); |
| 272 |
|
| 273 |
// size in bytes of name |
| 274 |
$nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS + 1]) << 8); |
| 275 |
|
| 276 |
// type of entry |
| 277 |
$type = ord($d[self::TYPE_POS]); |
| 278 |
|
| 279 |
// sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook) |
| 280 |
// sectorID of first sector of the short-stream container stream, if this entry is root entry |
| 281 |
$startBlock = self::getInt4d($d, self::START_BLOCK_POS); |
| 282 |
|
| 283 |
$size = self::getInt4d($d, self::SIZE_POS); |
| 284 |
|
| 285 |
$name = str_replace("\x00", '', substr($d, 0, $nameSize)); |
| 286 |
|
| 287 |
$this->props[] = [ |
| 288 |
'name' => $name, |
| 289 |
'type' => $type, |
| 290 |
'startBlock' => $startBlock, |
| 291 |
'size' => $size, |
| 292 |
]; |
| 293 |
|
| 294 |
// tmp helper to simplify checks |
| 295 |
$upName = strtoupper($name); |
| 296 |
|
| 297 |
// Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook) |
| 298 |
if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) { |
| 299 |
$this->wrkbook = count($this->props) - 1; |
| 300 |
} elseif ($upName === 'ROOT ENTRY' || $upName === 'R') { |
| 301 |
// Root entry |
| 302 |
$this->rootentry = count($this->props) - 1; |
| 303 |
} |
| 304 |
|
| 305 |
// Summary information |
| 306 |
if ($name == chr(5) . 'SummaryInformation') { |
| 307 |
$this->summaryInformation = count($this->props) - 1; |
| 308 |
} |
| 309 |
|
| 310 |
// Additional Document Summary information |
| 311 |
if ($name == chr(5) . 'DocumentSummaryInformation') { |
| 312 |
$this->documentSummaryInformation = count($this->props) - 1; |
| 313 |
} |
| 314 |
|
| 315 |
$offset += self::PROPERTY_STORAGE_BLOCK_SIZE; |
| 316 |
} |
| 317 |
} |
| 318 |
|
| 319 |
/** |
| 320 |
* Read 4 bytes of data at specified position. |
| 321 |
* |
| 322 |
* @param string $data |
| 323 |
* @param int $pos |
| 324 |
* |
| 325 |
* @return int |
| 326 |
*/ |
| 327 |
private static function getInt4d($data, $pos) |
| 328 |
{ |
| 329 |
if ($pos < 0) { |
| 330 |
// Invalid position |
| 331 |
throw new ReaderException('Parameter pos=' . $pos . ' is invalid.'); |
| 332 |
} |
| 333 |
|
| 334 |
$len = strlen($data); |
| 335 |
if ($len < $pos + 4) { |
| 336 |
$data .= str_repeat("\0", $pos + 4 - $len); |
| 337 |
} |
| 338 |
|
| 339 |
// FIX: represent numbers correctly on 64-bit system |
| 340 |
// http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 |
| 341 |
// Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems |
| 342 |
$_or_24 = ord($data[$pos + 3]); |
| 343 |
if ($_or_24 >= 128) { |
| 344 |
// negative number |
| 345 |
$_ord_24 = -abs((256 - $_or_24) << 24); |
| 346 |
} else { |
| 347 |
$_ord_24 = ($_or_24 & 127) << 24; |
| 348 |
} |
| 349 |
|
| 350 |
return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; |
| 351 |
} |
| 352 |
} |
| 353 |
|