CFB.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <?php
  2. namespace Nick\SecureSpreadsheet;
  3. class CFB
  4. {
  5. public function cfb_new($opts = null)
  6. {
  7. $o = new \stdClass();
  8. $this->init_cfb($o, $opts);
  9. return $o;
  10. }
  11. public function cfb_add(&$cfb, $name, $content, $opts = null)
  12. {
  13. $unsafe = $opts && $opts->unsafe;
  14. if (!$unsafe) $this->init_cfb($cfb);
  15. $file = !$unsafe && $this->find($cfb, $name);
  16. if (!$file) {
  17. $fpath = $cfb->FullPaths[0];
  18. if (substr($name, 0, strlen($fpath)) == $fpath) $fpath = $name;
  19. else {
  20. if (substr($fpath, -1) != "/") $fpath .= "/";
  21. $fpath = str_replace("//", "/", $fpath . $name);
  22. }
  23. $file = (['name' => $this->filename($name), 'type' => 2]);
  24. $file['content'] = ($content);
  25. $file['size'] = $content ? count($content) : 0;
  26. if ($opts) {
  27. if ($opts->CLSID) $file['clsid'] = $opts->CLSID;
  28. if ($opts->mt) $file['mt'] = $opts->mt;
  29. if ($opts->ct) $file['ct'] = $opts->ct;
  30. }
  31. $cfb->FileIndex[] = ($file);
  32. $cfb->FullPaths[] = ($fpath);
  33. if (!$unsafe) $this->cfb_gc($cfb);
  34. }
  35. return $file;
  36. }
  37. private function init_cfb(&$cfb, $opts = null)
  38. {
  39. $o = $opts ?? new \stdClass();
  40. $root = $o->root ?? "Root Entry";
  41. if (!isset($cfb->FullPaths)) $cfb->FullPaths = [];
  42. if (!isset($cfb->FileIndex)) $cfb->FileIndex = [];
  43. if (count($cfb->FullPaths) !== count($cfb->FileIndex)) throw new \Error("inconsistent CFB structure");
  44. if (count($cfb->FullPaths) === 0) {
  45. $cfb->FullPaths[0] = $root . "/";
  46. $cfb->FileIndex[0] = (['name' => $root, 'type' => 5]);
  47. }
  48. if (isset($o->CLSID)) $cfb->FileIndex[0]->clsid = $o->CLSID;
  49. $this->seed_cfb($cfb);
  50. }
  51. private function seed_cfb(&$cfb)
  52. {
  53. $nm = "\x01Sh33tJ5";
  54. if ($this->find($cfb, "/" . $nm)) return;
  55. $p = $this->new_buf(4);
  56. $p[0] = 55;
  57. $p[1] = $p[3] = 50;
  58. $p[2] = 54;
  59. $cfb->FileIndex[] = ((['name' => $nm, 'type' => 2, 'content' => $p, 'size' => 4, 'L' => 69, 'R' => 69, 'C' => 69]));
  60. $cfb->FullPaths[] = ($cfb->FullPaths[0] . $nm);
  61. $this->rebuild_cfb($cfb);
  62. }
  63. private function new_buf($sz): Buffer
  64. {
  65. $o = ($this->new_raw_buf($sz));
  66. $this->prep_blob($o, 0);
  67. return $o;
  68. }
  69. private function new_raw_buf($sz)
  70. {
  71. return new Buffer($sz);
  72. }
  73. private function prep_blob($blob, $pos)
  74. {
  75. $blob->l = $pos;
  76. }
  77. private function cfb_gc(&$cfb)
  78. {
  79. $this->rebuild_cfb($cfb, true);
  80. }
  81. private function rebuild_cfb(&$cfb, $f = false)
  82. {
  83. $HEADER_CLSID = '00000000000000000000000000000000';
  84. $this->init_cfb($cfb);
  85. $gc = false;
  86. $s = false;
  87. for ($i = count($cfb->FullPaths) - 1; $i >= 0; --$i) {
  88. $_file = $cfb->FileIndex[$i];
  89. // var_dump($_file);
  90. switch ($_file['type']) {
  91. case 0:
  92. if ($s) $gc = true;
  93. else {
  94. array_pop($cfb->FileIndex);
  95. array_pop($cfb->FullPaths);
  96. }
  97. break;
  98. case 1:
  99. case 2:
  100. case 5:
  101. $s = true;
  102. if (!isset($_file['R'], $_file['L'], $_file['C'])) $gc = true;
  103. if (isset($_file['R'], $_file['L'], $_file['C']) && $_file['R'] > -1 && $_file['L'] > -1 && $_file['R'] == $_file['L']) $gc = true;
  104. break;
  105. default:
  106. $gc = true;
  107. break;
  108. }
  109. }
  110. if (!$gc && !$f) return;
  111. $now = mktime(0, 0, 0, 1, 19, 1987);
  112. $j = 0;
  113. // Track which names exist
  114. $fullPaths = [];
  115. $data = [];
  116. for ($i = 0; $i < count($cfb->FullPaths); ++$i) {
  117. $fullPaths[$cfb->FullPaths[$i]] = true;
  118. if ($cfb->FileIndex[$i]['type'] === 0) continue;
  119. $data[] = ([$cfb->FullPaths[$i], $cfb->FileIndex[$i]]);
  120. }
  121. for ($i = 0; $i < count($data); ++$i) {
  122. $dad = $this->dirname($data[$i][0]);
  123. $s = $fullPaths[$dad];
  124. while (!$s) {
  125. while ($this->dirname($dad) && !$fullPaths[$this->dirname($dad)]) $dad = $this->dirname($dad);
  126. $data[] = ([$dad, ([
  127. 'name' => str_replace("/", "", $this->filename($dad)),
  128. 'type' => 1,
  129. 'clsid' => $HEADER_CLSID,
  130. 'ct' => $now, 'mt' => $now,
  131. 'content' => null
  132. ])]);
  133. // Add name to set
  134. $fullPaths[$dad] = true;
  135. $dad = dirname($data[$i][0]);
  136. $s = $fullPaths[$dad];
  137. }
  138. }
  139. usort($data, array(Self::class, "namecmp"));
  140. $cfb->FullPaths = [];
  141. $cfb->FileIndex = [];
  142. for ($i = 0; $i < count($data); ++$i) {
  143. $cfb->FullPaths[$i] = $data[$i][0];
  144. $cfb->FileIndex[$i] = $data[$i][1];
  145. }
  146. for ($i = 0; $i < count($data); ++$i) {
  147. $elt = $cfb->FileIndex[$i];
  148. $nm = $cfb->FullPaths[$i];
  149. // $elt['name'] = $nm;
  150. $elt['name'] = str_replace("/", "", $this->filename($nm));
  151. $elt['L'] = $elt['R'] = $elt['C'] = - ($elt['color'] = 1);
  152. if (isset($elt['content'])) {
  153. $elt['size'] = $elt['content'] ? count($elt['content']) : 0;
  154. } else {
  155. $elt['size'] = 0;
  156. }
  157. $elt['start'] = 0;
  158. $elt['clsid'] = ($elt['clsid'] ?? $HEADER_CLSID);
  159. if ($i === 0) {
  160. $elt['C'] = count($data) > 1 ? 1 : -1;
  161. $elt['size'] = 0;
  162. $elt['type'] = 5;
  163. } else if (substr($nm, -1) == "/") {
  164. for ($j = $i + 1; $j < count($data); ++$j) if ($this->dirname($cfb->FullPaths[$j]) == $nm) break;
  165. $elt['C'] = $j >= count($data) ? -1 : $j;
  166. for ($j = $i + 1; $j < count($data); ++$j) if ($this->dirname($cfb->FullPaths[$j]) == $this->dirname($nm)) break;
  167. $elt['R'] = $j >= count($data) ? -1 : $j;
  168. $elt['type'] = 1;
  169. } else {
  170. if ($this->dirname($cfb->FullPaths[$i + 1] ?? "") == $this->dirname($nm)) $elt['R'] = $i + 1;
  171. $elt['type'] = 2;
  172. }
  173. $cfb->FileIndex[$i] = $elt;
  174. $cfb->FullPaths[$i] = $nm;
  175. }
  176. // var_dump($cfb);
  177. }
  178. private function find($cfb, $path)
  179. {
  180. $UCFullPaths = array_map(function ($i) {
  181. return strtoupper($i);
  182. }, $cfb->FullPaths);
  183. $UCPaths = array_map(function ($i) {
  184. $y = explode("/", $i);
  185. return $y[count($y) - (substr($i, -1) == "/" ? 2 : 1)];
  186. }, $UCFullPaths);
  187. $k = false;
  188. if (ord($path[0]) === 47 /* "/" */) {
  189. $k = true;
  190. $path = substr($UCFullPaths[0], 0, -1) . $path;
  191. } else $k = strpos($path, '/') != false;
  192. $UCPath = strtoupper($path);
  193. $w = $k === true ? array_search($UCPath, $UCFullPaths) : array_search($UCPath, $UCPaths);
  194. if ($w > -1) return $cfb->FileIndex[$w];
  195. return false;
  196. }
  197. private static function namecmp($l, $r)
  198. {
  199. $L = explode('/', $l[0]);
  200. $R = explode('/', $r[0]);
  201. $c = 0;
  202. $Z = min(count($L), count($R));
  203. for ($i = 0; $i < $Z; ++$i) {
  204. if (($c = strlen($L[$i]) - strlen($R[$i]))) return $c;
  205. if ($L[$i] != $R[$i]) return $L[$i] < $R[$i] ? -1 : 1;
  206. }
  207. return count($L) - count($R);
  208. }
  209. private function dirname($p)
  210. {
  211. $pl = strlen($p);
  212. if ($pl == 0) {
  213. return $p;
  214. }
  215. if ($p[($pl - 1)] == "/") {
  216. if ((strpos(substr($p, 0, -1), '/') === false)) {
  217. return $p;
  218. }
  219. return $this->dirname(substr($p, 0, -1));
  220. }
  221. $c = strrpos($p, '/');
  222. return ($c === false) ? $p : substr($p, 0, $c + 1);
  223. }
  224. private function filename($p)
  225. {
  226. if ($p[(strlen($p) - 1)] == "/") {
  227. return $this->filename(substr($p, 0, -1));
  228. }
  229. $c = strrpos($p, '/');
  230. return ($c === false) ? $p : substr($p, $c + 1);
  231. }
  232. public function write(&$cfb)
  233. {
  234. return $this->_write($cfb);
  235. }
  236. private function _write(&$cfb)
  237. {
  238. $HEADER_SIG = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
  239. $ENDOFCHAIN = -2;
  240. $this->rebuild_cfb($cfb);
  241. $L = (function ($cfb) {
  242. $mini_size = 0;
  243. $fat_size = 0;
  244. for ($i = 0; $i < count($cfb->FileIndex); ++$i) {
  245. $file = $cfb->FileIndex[$i];
  246. if (!isset($file['content'])) continue;
  247. $flen = count($file['content']);
  248. if ($flen > 0) {
  249. if ($flen < 0x1000) {
  250. $mini_size += ($flen + 0x3F) >> 6;
  251. } else {
  252. $fat_size += ($flen + 0x01FF) >> 9;
  253. }
  254. }
  255. }
  256. $dir_cnt = (count($cfb->FullPaths) + 3) >> 2;
  257. $mini_cnt = ($mini_size + 7) >> 3;
  258. $mfat_cnt = ($mini_size + 0x7F) >> 7;
  259. $fat_base = $mini_cnt + $fat_size + $dir_cnt + $mfat_cnt;
  260. $fat_cnt = ($fat_base + 0x7F) >> 7;
  261. $difat_cnt = $fat_cnt <= 109 ? 0 : ceil(($fat_cnt - 109) / 0x7F);
  262. while ((($fat_base + $fat_cnt + $difat_cnt + 0x7F) >> 7) > $fat_cnt) $difat_cnt = ++$fat_cnt <= 109 ? 0 : ceil(($fat_cnt - 109) / 0x7F);
  263. $L = [1, $difat_cnt, $fat_cnt, $mfat_cnt, $dir_cnt, $fat_size, $mini_size, 0];
  264. $cfb->FileIndex[0]['size'] = $mini_size << 6;
  265. $L[7] = ($cfb->FileIndex[0]['start'] = $L[0] + $L[1] + $L[2] + $L[3] + $L[4] + $L[5]) + (($L[6] + 7) >> 3);
  266. return $L;
  267. })($cfb);
  268. $o = $this->new_buf($L[7] << 9);
  269. $i = 0;
  270. $T = 0; {
  271. for ($i = 0; $i < 8; ++$i) $o->write_shift(1, $HEADER_SIG[$i]);
  272. for ($i = 0; $i < 8; ++$i) $o->write_shift(2, 0);
  273. $o->write_shift(2, 0x003E);
  274. $o->write_shift(2, 0x0003);
  275. $o->write_shift(2, 0xFFFE);
  276. $o->write_shift(2, 0x0009);
  277. $o->write_shift(2, 0x0006); // 34
  278. for ($i = 0; $i < 3; ++$i) $o->write_shift(2, 0); // 37
  279. $o->write_shift(4, 0);
  280. $o->write_shift(4, $L[2]);
  281. $o->write_shift(4, $L[0] + $L[1] + $L[2] + $L[3] - 1);
  282. $o->write_shift(4, 0);
  283. $o->write_shift(4, 1 << 12);
  284. $o->write_shift(4, $L[3] ? $L[0] + $L[1] + $L[2] - 1 : $ENDOFCHAIN);
  285. $o->write_shift(4, $L[3]);
  286. $o->write_shift(-4, $L[1] ? $L[0] - 1 : $ENDOFCHAIN);
  287. $o->write_shift(4, $L[1]);
  288. for ($i = 0; $i < 109; ++$i) $o->write_shift(-4, $i < $L[2] ? $L[1] + $i : -1);
  289. }
  290. if ($L[1]) {
  291. for ($T = 0; $T < $L[1]; ++$T) {
  292. for (; $i < 236 + $T * 127; ++$i) $o->write_shift(-4, $i < $L[2] ? $L[1] + $i : -1);
  293. $o->write_shift(-4, $T === $L[1] - 1 ? $ENDOFCHAIN : $T + 1);
  294. }
  295. }
  296. $chainit = function ($w, &$T, &$i, &$o) {
  297. for ($T += $w; $i < $T - 1; ++$i) $o->write_shift(-4, $i + 1);
  298. if ($w) {
  299. ++$i;
  300. $o->write_shift(-4, -2);
  301. }
  302. };
  303. $T = $i = 0;
  304. for ($T += $L[1]; $i < $T; ++$i) $o->write_shift(-4, -4);
  305. for ($T += $L[2]; $i < $T; ++$i) $o->write_shift(-4, -3);
  306. $chainit($L[3], $T, $i, $o);
  307. $chainit($L[4], $T, $i, $o);
  308. $j = 0;
  309. $flen = 0;
  310. $file = $cfb->FileIndex[0];
  311. for (; $j < count($cfb->FileIndex); ++$j) {
  312. $file = $cfb->FileIndex[$j];
  313. if (!isset($file['content'])) continue;
  314. $flen = count($file['content']);
  315. if ($flen < 0x1000) continue;
  316. $cfb->FileIndex[$j]['start'] = $T;
  317. $chainit(($flen + 0x01FF) >> 9, $T, $i, $o);
  318. }
  319. $chainit(($L[6] + 7) >> 3, $T, $i, $o);
  320. while ($o->l & 0x1FF) $o->write_shift(-4, $ENDOFCHAIN);
  321. $T = $i = 0;
  322. for ($j = 0; $j < count($cfb->FileIndex); ++$j) {
  323. $file = $cfb->FileIndex[$j];
  324. if (!isset($file['content'])) continue;
  325. $flen = count($file['content']);
  326. if (!$flen || $flen >= 0x1000) continue;
  327. $cfb->FileIndex[$j]['start'] = $T;
  328. $chainit(($flen + 0x3F) >> 6, $T, $i, $o);
  329. }
  330. while ($o->l & 0x1FF) $o->write_shift(-4, $ENDOFCHAIN);
  331. for ($i = 0; $i < $L[4] << 2; ++$i) {
  332. $nm = $cfb->FullPaths[$i];
  333. if (!$nm || strlen($nm) === 0) {
  334. for ($j = 0; $j < 17; ++$j) $o->write_shift(4, 0);
  335. for ($j = 0; $j < 3; ++$j) $o->write_shift(4, -1);
  336. for ($j = 0; $j < 12; ++$j) $o->write_shift(4, 0);
  337. continue;
  338. }
  339. $file = $cfb->FileIndex[$i];
  340. if ($i === 0) {
  341. $file['start'] = $cfb->FileIndex[$i]['size'] ? ($cfb->FileIndex[$i]['start'] - 1) : $ENDOFCHAIN;
  342. }
  343. $_nm = $file['name'];
  344. if (strlen($_nm) > 32) {
  345. $_nm = substr($_nm, 0, 32);
  346. }
  347. $flen = 2 * (strlen($_nm) + 1);
  348. $o->write_shift(64, $_nm, "utf16le");
  349. $o->write_shift(2, $flen);
  350. $o->write_shift(1, $file['type']);
  351. $o->write_shift(1, $file['color']);
  352. $o->write_shift(-4, $file['L']);
  353. $o->write_shift(-4, $file['R']);
  354. $o->write_shift(-4, $file['C']);
  355. if (!$file['clsid']) for ($j = 0; $j < 4; ++$j) $o->write_shift(4, 0);
  356. else $o->write_shift(16, $file['clsid'], "hex");
  357. $o->write_shift(4, $file['state'] ?? 0);
  358. $o->write_shift(4, 0);
  359. $o->write_shift(4, 0);
  360. $o->write_shift(4, 0);
  361. $o->write_shift(4, 0);
  362. $o->write_shift(4, $file['start']);
  363. $o->write_shift(4, $file['size']);
  364. $o->write_shift(4, 0);
  365. }
  366. for ($i = 1; $i < count($cfb->FileIndex); ++$i) {
  367. $file = $cfb->FileIndex[$i];
  368. if ($file['size'] >= 0x1000) {
  369. $o->l = ($file['start'] + 1) << 9;
  370. for ($j = 0; $j < $file['size']; ++$j) $o->write_shift(1, $file['content'][$j]);
  371. for (; $j & 0x1FF; ++$j) $o->write_shift(1, 0);
  372. }
  373. }
  374. for ($i = 1; $i < count($cfb->FileIndex); ++$i) {
  375. $file = $cfb->FileIndex[$i];
  376. if ($file['size'] > 0 && $file['size'] < 0x1000) {
  377. for ($j = 0; $j < $file['size']; ++$j) $o->write_shift(1, $file['content'][$j]);
  378. for (; $j & 0x3F; ++$j) $o->write_shift(1, 0);
  379. }
  380. }
  381. while ($o->l < count($o)) $o->write_shift(1, 0);
  382. return $o;
  383. }
  384. }