@cj-tech-master/excelts 9.6.0 → 9.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/browser/modules/archive/io/random-access.d.ts +1 -1
  2. package/dist/browser/modules/excel/workbook.browser.d.ts +1 -1
  3. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  4. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  5. package/dist/browser/modules/pdf/excel-bridge.d.ts +32 -0
  6. package/dist/browser/modules/pdf/excel-bridge.js +67 -1
  7. package/dist/browser/modules/pdf/word-bridge.d.ts +20 -15
  8. package/dist/browser/modules/pdf/word-bridge.js +49 -34
  9. package/dist/browser/modules/stream/common/consumers.d.ts +2 -1
  10. package/dist/browser/modules/word/advanced/diff.js +125 -13
  11. package/dist/browser/modules/word/advanced/drawing-shapes.js +3 -0
  12. package/dist/browser/modules/word/bridge/excel-bridge.js +21 -1
  13. package/dist/browser/modules/word/builder/document-handle.d.ts +2 -0
  14. package/dist/browser/modules/word/builder/document-handle.js +14 -2
  15. package/dist/browser/modules/word/builder/paragraph-builders.js +10 -1
  16. package/dist/browser/modules/word/builder/run-builders.d.ts +19 -2
  17. package/dist/browser/modules/word/builder/run-builders.js +2 -6
  18. package/dist/browser/modules/word/convert/odt/odt.js +6 -1
  19. package/dist/browser/modules/word/layout/layout-full.d.ts +12 -0
  20. package/dist/browser/modules/word/layout/layout-full.js +74 -9
  21. package/dist/browser/modules/word/layout/layout-model.d.ts +12 -0
  22. package/dist/browser/modules/word/query/merge.js +26 -10
  23. package/dist/browser/modules/word/query/split.js +68 -2
  24. package/dist/browser/modules/word/reader/docx-reader.js +23 -0
  25. package/dist/browser/modules/word/security/cfb-reader.d.ts +14 -3
  26. package/dist/browser/modules/word/security/cfb-reader.js +271 -153
  27. package/dist/browser/modules/word/security/document-protection.js +10 -4
  28. package/dist/browser/modules/word/security/encryption.js +194 -32
  29. package/dist/browser/modules/word/types.d.ts +17 -0
  30. package/dist/browser/modules/word/units.d.ts +10 -4
  31. package/dist/browser/modules/word/units.js +10 -4
  32. package/dist/browser/modules/word/writer/document-writer.js +28 -4
  33. package/dist/browser/modules/word/writer/docx-packager.js +45 -5
  34. package/dist/browser/modules/word/writer/image-writer.d.ts +1 -1
  35. package/dist/browser/modules/word/writer/image-writer.js +2 -2
  36. package/dist/browser/modules/word/writer/render-context.d.ts +15 -0
  37. package/dist/browser/modules/word/writer/run-writer.js +8 -4
  38. package/dist/browser/modules/word/writer/section-writer.js +46 -35
  39. package/dist/browser/modules/word/writer/streaming-writer.js +4 -0
  40. package/dist/browser/modules/word/writer/styles-writer.js +11 -0
  41. package/dist/browser/modules/word/writer/table-writer.js +6 -0
  42. package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  43. package/dist/cjs/modules/pdf/excel-bridge.js +67 -0
  44. package/dist/cjs/modules/pdf/word-bridge.js +49 -34
  45. package/dist/cjs/modules/word/advanced/diff.js +125 -13
  46. package/dist/cjs/modules/word/advanced/drawing-shapes.js +3 -0
  47. package/dist/cjs/modules/word/bridge/excel-bridge.js +21 -1
  48. package/dist/cjs/modules/word/builder/document-handle.js +14 -2
  49. package/dist/cjs/modules/word/builder/paragraph-builders.js +10 -1
  50. package/dist/cjs/modules/word/builder/run-builders.js +2 -6
  51. package/dist/cjs/modules/word/convert/odt/odt.js +6 -1
  52. package/dist/cjs/modules/word/layout/layout-full.js +74 -9
  53. package/dist/cjs/modules/word/query/merge.js +26 -10
  54. package/dist/cjs/modules/word/query/split.js +68 -2
  55. package/dist/cjs/modules/word/reader/docx-reader.js +23 -0
  56. package/dist/cjs/modules/word/security/cfb-reader.js +271 -153
  57. package/dist/cjs/modules/word/security/document-protection.js +10 -4
  58. package/dist/cjs/modules/word/security/encryption.js +193 -31
  59. package/dist/cjs/modules/word/units.js +10 -4
  60. package/dist/cjs/modules/word/writer/document-writer.js +28 -4
  61. package/dist/cjs/modules/word/writer/docx-packager.js +45 -5
  62. package/dist/cjs/modules/word/writer/image-writer.js +2 -2
  63. package/dist/cjs/modules/word/writer/run-writer.js +8 -4
  64. package/dist/cjs/modules/word/writer/section-writer.js +46 -35
  65. package/dist/cjs/modules/word/writer/streaming-writer.js +4 -0
  66. package/dist/cjs/modules/word/writer/styles-writer.js +11 -0
  67. package/dist/cjs/modules/word/writer/table-writer.js +6 -0
  68. package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  69. package/dist/esm/modules/pdf/excel-bridge.js +67 -1
  70. package/dist/esm/modules/pdf/word-bridge.js +49 -34
  71. package/dist/esm/modules/word/advanced/diff.js +125 -13
  72. package/dist/esm/modules/word/advanced/drawing-shapes.js +3 -0
  73. package/dist/esm/modules/word/bridge/excel-bridge.js +21 -1
  74. package/dist/esm/modules/word/builder/document-handle.js +14 -2
  75. package/dist/esm/modules/word/builder/paragraph-builders.js +10 -1
  76. package/dist/esm/modules/word/builder/run-builders.js +2 -6
  77. package/dist/esm/modules/word/convert/odt/odt.js +6 -1
  78. package/dist/esm/modules/word/layout/layout-full.js +74 -9
  79. package/dist/esm/modules/word/query/merge.js +26 -10
  80. package/dist/esm/modules/word/query/split.js +68 -2
  81. package/dist/esm/modules/word/reader/docx-reader.js +23 -0
  82. package/dist/esm/modules/word/security/cfb-reader.js +271 -153
  83. package/dist/esm/modules/word/security/document-protection.js +10 -4
  84. package/dist/esm/modules/word/security/encryption.js +194 -32
  85. package/dist/esm/modules/word/units.js +10 -4
  86. package/dist/esm/modules/word/writer/document-writer.js +28 -4
  87. package/dist/esm/modules/word/writer/docx-packager.js +45 -5
  88. package/dist/esm/modules/word/writer/image-writer.js +2 -2
  89. package/dist/esm/modules/word/writer/run-writer.js +8 -4
  90. package/dist/esm/modules/word/writer/section-writer.js +46 -35
  91. package/dist/esm/modules/word/writer/streaming-writer.js +4 -0
  92. package/dist/esm/modules/word/writer/styles-writer.js +11 -0
  93. package/dist/esm/modules/word/writer/table-writer.js +6 -0
  94. package/dist/iife/excelts.iife.js +20 -8
  95. package/dist/iife/excelts.iife.js.map +1 -1
  96. package/dist/iife/excelts.iife.min.js +2 -2
  97. package/dist/types/modules/archive/io/random-access.d.ts +1 -1
  98. package/dist/types/modules/excel/workbook.browser.d.ts +1 -1
  99. package/dist/types/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  100. package/dist/types/modules/pdf/excel-bridge.d.ts +32 -0
  101. package/dist/types/modules/pdf/word-bridge.d.ts +20 -15
  102. package/dist/types/modules/stream/common/consumers.d.ts +2 -1
  103. package/dist/types/modules/word/builder/document-handle.d.ts +2 -0
  104. package/dist/types/modules/word/builder/run-builders.d.ts +19 -2
  105. package/dist/types/modules/word/layout/layout-full.d.ts +12 -0
  106. package/dist/types/modules/word/layout/layout-model.d.ts +12 -0
  107. package/dist/types/modules/word/security/cfb-reader.d.ts +14 -3
  108. package/dist/types/modules/word/types.d.ts +17 -0
  109. package/dist/types/modules/word/units.d.ts +10 -4
  110. package/dist/types/modules/word/writer/image-writer.d.ts +1 -1
  111. package/dist/types/modules/word/writer/render-context.d.ts +15 -0
  112. package/package.json +2 -2
@@ -219,12 +219,51 @@ function readCfb(buffer) {
219
219
  // =============================================================================
220
220
  // CFB Writer (v3, sector size 512)
221
221
  // =============================================================================
222
+ const MINI_SECTOR_SIZE = 64;
223
+ const MINI_STREAM_CUTOFF = 4096;
224
+ const NOSTREAM = 0xffffffff;
225
+ /** Create a directory node with the sibling/child pointers in their unset state. */
226
+ function makeDirNode(name, type, data) {
227
+ return {
228
+ name,
229
+ type,
230
+ data,
231
+ children: [],
232
+ left: NOSTREAM,
233
+ right: NOSTREAM,
234
+ child: NOSTREAM,
235
+ startSector: ENDOFCHAIN,
236
+ size: data?.length ?? 0
237
+ };
238
+ }
239
+ /**
240
+ * MS-CFB sibling ordering: compare by UTF-16 code-unit length first, then
241
+ * by upper-cased code units. This is the exact ordering Office uses to lay
242
+ * out the red-black directory tree; getting it wrong makes Word reject the
243
+ * container even though the bytes are otherwise valid.
244
+ */
245
+ function compareCfbNames(a, b) {
246
+ if (a.length !== b.length) {
247
+ return a.length - b.length;
248
+ }
249
+ const ua = a.toUpperCase();
250
+ const ub = b.toUpperCase();
251
+ if (ua < ub) {
252
+ return -1;
253
+ }
254
+ if (ua > ub) {
255
+ return 1;
256
+ }
257
+ return 0;
258
+ }
222
259
  /**
223
260
  * Write a set of named stream entries into a CFB (OLE2 Compound File) container.
224
261
  *
225
- * Produces a minimal v3 CFB with 512-byte sectors. Does not use mini-streams
226
- * (all data is stored in regular sectors). This is suitable for encrypted Office
227
- * documents where every stream exceeds 4096 bytes.
262
+ * Produces a v3 CFB with 512-byte sectors. Streams smaller than 4096 bytes are
263
+ * stored in the mini-stream (64-byte mini-sectors) exactly as Office does;
264
+ * larger streams use regular sectors. Entries may declare a `path` to nest the
265
+ * stream inside one or more storages — required for the `\u0006DataSpaces`
266
+ * structure that Office demands in encrypted documents.
228
267
  *
229
268
  * @param entries - Named stream entries to include.
230
269
  * @returns The CFB file as a Uint8Array.
@@ -232,183 +271,262 @@ function readCfb(buffer) {
232
271
  function writeCfb(entries) {
233
272
  const SECTOR_SIZE = 512;
234
273
  const DIR_ENTRY_SIZE = 128;
235
- // Compute sector count for each entry
236
- const entrySectors = entries.map(e => Math.ceil(e.data.length / SECTOR_SIZE));
237
- // Directory entries: Root Entry + one per stream entry
238
- const dirEntryCount = 1 + entries.length;
239
- const dirSectors = Math.ceil((dirEntryCount * DIR_ENTRY_SIZE) / SECTOR_SIZE);
240
- // Total data sectors
241
- const totalDataSectors = entrySectors.reduce((a, b) => a + b, 0);
242
- // FAT entries needed: directory sectors + data sectors + FAT sectors themselves
243
- // We solve iteratively since FAT sectors count depends on total sector count
274
+ // ---------------------------------------------------------------------------
275
+ // 1. Build the directory tree (root storage + nested storages + streams).
276
+ // ---------------------------------------------------------------------------
277
+ const root = makeDirNode("Root Entry", 5);
278
+ const getOrCreateStorage = (parent, name) => {
279
+ let node = parent.children.find(c => c.type === 1 && c.name === name);
280
+ if (!node) {
281
+ node = makeDirNode(name, 1);
282
+ parent.children.push(node);
283
+ }
284
+ return node;
285
+ };
286
+ for (const entry of entries) {
287
+ let parent = root;
288
+ for (const seg of entry.path ?? []) {
289
+ parent = getOrCreateStorage(parent, seg);
290
+ }
291
+ parent.children.push(makeDirNode(entry.name, 2, entry.data));
292
+ }
293
+ // ---------------------------------------------------------------------------
294
+ // 2. Flatten the tree into a directory-entry array in DFS order and assign
295
+ // each node a directory index. Build the red-black sibling tree for each
296
+ // storage's children using the CFB name ordering.
297
+ // ---------------------------------------------------------------------------
298
+ const dir = [];
299
+ const assignIndices = (node) => {
300
+ dir.push(node);
301
+ for (const child of node.children) {
302
+ assignIndices(child);
303
+ }
304
+ };
305
+ assignIndices(root);
306
+ const indexOf = new Map();
307
+ dir.forEach((n, i) => indexOf.set(n, i));
308
+ // Build a balanced BST over the (sorted) sibling list. We produce a valid
309
+ // search tree; Office does not require strict red-black balancing, only a
310
+ // consistent ordering, so a balanced BST is accepted.
311
+ const buildSiblingTree = (siblings) => {
312
+ const sorted = siblings.slice().sort((a, b) => compareCfbNames(a.name, b.name));
313
+ const build = (lo, hi) => {
314
+ if (lo > hi) {
315
+ return NOSTREAM;
316
+ }
317
+ const mid = (lo + hi) >> 1;
318
+ const node = sorted[mid];
319
+ node.left = build(lo, mid - 1);
320
+ node.right = build(mid + 1, hi);
321
+ return indexOf.get(node);
322
+ };
323
+ return build(0, sorted.length - 1);
324
+ };
325
+ // Root + every storage gets a child pointer to the tree root of its children.
326
+ const linkChildren = (node) => {
327
+ if (node.children.length > 0) {
328
+ node.child = buildSiblingTree(node.children);
329
+ }
330
+ for (const child of node.children) {
331
+ linkChildren(child);
332
+ }
333
+ };
334
+ linkChildren(root);
335
+ // ---------------------------------------------------------------------------
336
+ // 3. Split streams into mini-stream (<4096) vs regular sectors. The
337
+ // mini-stream itself is a chain of regular sectors owned by the root entry.
338
+ // ---------------------------------------------------------------------------
339
+ const streamNodes = dir.filter(n => n.type === 2);
340
+ const miniNodes = streamNodes.filter(n => n.size > 0 && n.size < MINI_STREAM_CUTOFF);
341
+ const regularNodes = streamNodes.filter(n => n.size >= MINI_STREAM_CUTOFF);
342
+ // Assemble the mini-stream and the mini-FAT.
343
+ let miniStream = new Uint8Array(0);
344
+ const miniFat = [];
345
+ {
346
+ const parts = [];
347
+ let miniSectorIdx = 0;
348
+ let totalLen = 0;
349
+ for (const node of miniNodes) {
350
+ const sectorCount = Math.ceil(node.size / MINI_SECTOR_SIZE);
351
+ node.startSector = miniSectorIdx;
352
+ for (let i = 0; i < sectorCount; i++) {
353
+ miniFat.push(i < sectorCount - 1 ? miniSectorIdx + 1 : ENDOFCHAIN);
354
+ miniSectorIdx++;
355
+ }
356
+ const padded = new Uint8Array(sectorCount * MINI_SECTOR_SIZE);
357
+ padded.set(node.data);
358
+ parts.push(padded);
359
+ totalLen += padded.length;
360
+ }
361
+ miniStream = new Uint8Array(totalLen);
362
+ let off = 0;
363
+ for (const p of parts) {
364
+ miniStream.set(p, off);
365
+ off += p.length;
366
+ }
367
+ }
368
+ root.size = miniStream.length;
369
+ // ---------------------------------------------------------------------------
370
+ // 4. Lay out regular sectors.
371
+ // Layout order in file: [FAT sectors][Directory][MiniFAT][MiniStream][Regular streams]
372
+ // We compute counts first, then assign sector indices, then fill the FAT.
373
+ // ---------------------------------------------------------------------------
374
+ const dirSectors = Math.ceil((dir.length * DIR_ENTRY_SIZE) / SECTOR_SIZE);
375
+ const miniFatBytes = miniFat.length * 4;
376
+ const miniFatSectors = miniFatBytes > 0 ? Math.ceil(miniFatBytes / SECTOR_SIZE) : 0;
377
+ const miniStreamSectors = miniStream.length > 0 ? Math.ceil(miniStream.length / SECTOR_SIZE) : 0;
378
+ const regularSectorCounts = regularNodes.map(n => Math.ceil(n.size / SECTOR_SIZE));
379
+ const totalRegularStreamSectors = regularSectorCounts.reduce((a, b) => a + b, 0);
380
+ const nonFatSectors = dirSectors + miniFatSectors + miniStreamSectors + totalRegularStreamSectors;
381
+ // Solve for the number of FAT sectors (each FAT sector indexes 128 sectors).
244
382
  let fatSectors = 1;
245
383
  while (true) {
246
- const totalSectors = dirSectors + totalDataSectors + fatSectors;
247
- const fatCapacity = fatSectors * (SECTOR_SIZE / 4);
248
- if (fatCapacity >= totalSectors) {
384
+ const totalSectors = nonFatSectors + fatSectors;
385
+ if (fatSectors * (SECTOR_SIZE / 4) >= totalSectors) {
249
386
  break;
250
387
  }
251
388
  fatSectors++;
252
389
  }
253
- const totalSectors = dirSectors + totalDataSectors + fatSectors;
254
- const fileSize = (1 + totalSectors) * SECTOR_SIZE; // +1 for header sector
390
+ // The header stores up to 109 DIFAT entries inline; beyond that a DIFAT
391
+ // sector chain is required, which this minimal writer does not emit. Bail
392
+ // out rather than silently produce a corrupt container. 109 FAT sectors
393
+ // cover ~6.8 MB of sectors → tens of MB of stream data, far larger than any
394
+ // realistic EncryptedPackage.
395
+ if (fatSectors > 109) {
396
+ throw new errors_1.DocxParseError(`CFB writer: ${fatSectors} FAT sectors exceeds the 109-entry header DIFAT ` +
397
+ `limit (input too large for the minimal v3 writer).`);
398
+ }
399
+ const totalSectors = nonFatSectors + fatSectors;
400
+ const fileSize = (1 + totalSectors) * SECTOR_SIZE; // +1 header sector
255
401
  const output = new Uint8Array(fileSize);
256
402
  const view = new DataView(output.buffer);
257
- // --- Header (sector 0 area, 512 bytes) ---
258
- // Signature
403
+ // Assign sector ranges.
404
+ let cursor = 0;
405
+ const fatStart = cursor;
406
+ cursor += fatSectors;
407
+ const dirStart = cursor;
408
+ cursor += dirSectors;
409
+ const miniFatStart = miniFatSectors > 0 ? cursor : ENDOFCHAIN;
410
+ cursor += miniFatSectors;
411
+ const miniStreamStart = miniStreamSectors > 0 ? cursor : ENDOFCHAIN;
412
+ cursor += miniStreamSectors;
413
+ // Regular stream start sectors.
414
+ for (let i = 0; i < regularNodes.length; i++) {
415
+ regularNodes[i].startSector = cursor;
416
+ cursor += regularSectorCounts[i];
417
+ }
418
+ root.startSector = miniStreamStart;
419
+ // ---------------------------------------------------------------------------
420
+ // 5. Header.
421
+ // ---------------------------------------------------------------------------
259
422
  const sig = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
260
423
  for (let i = 0; i < 8; i++) {
261
424
  output[i] = sig[i];
262
425
  }
263
- // Minor version = 0x003E, Major version = 0x0003 (v3)
264
- view.setUint16(24, 0x003e, true);
265
- view.setUint16(26, 0x0003, true);
266
- // Byte order = 0xFFFE (little-endian)
267
- view.setUint16(28, 0xfffe, true);
268
- // Sector size power = 9 (2^9 = 512)
269
- view.setUint16(30, 9, true);
270
- // Mini sector size power = 6 (2^6 = 64)
271
- view.setUint16(32, 6, true);
272
- // Total sectors in directory (v3: must be 0)
273
- view.setUint32(40, 0, true);
274
- // Total FAT sectors
426
+ view.setUint16(24, 0x003e, true); // minor version
427
+ view.setUint16(26, 0x0003, true); // major version (v3)
428
+ view.setUint16(28, 0xfffe, true); // byte order LE
429
+ view.setUint16(30, 9, true); // sector shift (2^9 = 512)
430
+ view.setUint16(32, 6, true); // mini sector shift (2^6 = 64)
431
+ view.setUint32(40, 0, true); // number of directory sectors (v3: 0)
275
432
  view.setUint32(44, fatSectors, true);
276
- // First directory sector SECT
277
- // Layout: [FAT sectors] [Directory sectors] [Data sectors]
278
- const firstDirSector = fatSectors;
279
- view.setUint32(48, firstDirSector, true);
280
- // Transaction signature number
281
- view.setUint32(52, 0, true);
282
- // Mini stream cutoff = 0 (all streams stored in regular sectors)
283
- view.setUint32(56, 0, true);
284
- // First mini FAT sector = ENDOFCHAIN (none)
285
- view.setUint32(60, ENDOFCHAIN, true);
286
- // Mini FAT sector count = 0
287
- view.setUint32(64, 0, true);
288
- // First DIFAT sector = ENDOFCHAIN (none needed, <=109 FAT sectors)
289
- view.setUint32(68, ENDOFCHAIN, true);
290
- // DIFAT sector count = 0
291
- view.setUint32(72, 0, true);
292
- // DIFAT array in header (109 entries starting at offset 76)
433
+ view.setUint32(48, dirStart, true); // first directory sector
434
+ view.setUint32(52, 0, true); // transaction signature
435
+ view.setUint32(56, MINI_STREAM_CUTOFF, true); // mini stream cutoff
436
+ view.setUint32(60, miniFatStart, true); // first mini-FAT sector
437
+ view.setUint32(64, miniFatSectors, true); // mini-FAT sector count
438
+ view.setUint32(68, ENDOFCHAIN, true); // first DIFAT sector
439
+ view.setUint32(72, 0, true); // DIFAT sector count
440
+ // DIFAT in header (109 entries): point at the FAT sectors.
293
441
  for (let i = 0; i < 109; i++) {
294
- view.setUint32(76 + i * 4, i < fatSectors ? i : FREESECT, true);
442
+ view.setUint32(76 + i * 4, i < fatSectors ? fatStart + i : FREESECT, true);
295
443
  }
296
- // --- Build FAT ---
297
- const fatOffset = SECTOR_SIZE; // FAT starts at sector 0 in file
298
- const fatView = new DataView(output.buffer, fatOffset, fatSectors * SECTOR_SIZE);
299
- // Initialize all FAT entries to FREESECT
300
- for (let i = 0; i < fatSectors * (SECTOR_SIZE / 4); i++) {
444
+ // ---------------------------------------------------------------------------
445
+ // 6. FAT.
446
+ // ---------------------------------------------------------------------------
447
+ const fatFileOffset = (1 + fatStart) * SECTOR_SIZE;
448
+ const fatEntryCount = fatSectors * (SECTOR_SIZE / 4);
449
+ const fatView = new DataView(output.buffer, fatFileOffset, fatSectors * SECTOR_SIZE);
450
+ for (let i = 0; i < fatEntryCount; i++) {
301
451
  fatView.setUint32(i * 4, FREESECT, true);
302
452
  }
303
- let sectorIdx = 0;
304
- // FAT sectors themselves are marked as 0xFFFFFFFD (FATSECT)
453
+ // Helper: write a chain of `count` sectors starting at `start` into the FAT.
454
+ const writeFatChain = (start, count) => {
455
+ for (let i = 0; i < count; i++) {
456
+ const sec = start + i;
457
+ fatView.setUint32(sec * 4, i < count - 1 ? sec + 1 : ENDOFCHAIN, true);
458
+ }
459
+ };
460
+ // FAT sectors themselves are marked FATSECT (0xFFFFFFFD).
305
461
  for (let i = 0; i < fatSectors; i++) {
306
- fatView.setUint32(sectorIdx * 4, 0xfffffffd, true);
307
- sectorIdx++;
462
+ fatView.setUint32((fatStart + i) * 4, 0xfffffffd, true);
308
463
  }
309
- // Directory sectors chain
310
- for (let i = 0; i < dirSectors; i++) {
311
- const next = i < dirSectors - 1 ? sectorIdx + 1 : ENDOFCHAIN;
312
- fatView.setUint32(sectorIdx * 4, next, true);
313
- sectorIdx++;
464
+ writeFatChain(dirStart, dirSectors);
465
+ if (miniFatSectors > 0) {
466
+ writeFatChain(miniFatStart, miniFatSectors);
314
467
  }
315
- // Data sectors for each entry
316
- const entryStartSectors = [];
317
- for (let e = 0; e < entries.length; e++) {
318
- entryStartSectors.push(sectorIdx);
319
- for (let i = 0; i < entrySectors[e]; i++) {
320
- const next = i < entrySectors[e] - 1 ? sectorIdx + 1 : ENDOFCHAIN;
321
- fatView.setUint32(sectorIdx * 4, next, true);
322
- sectorIdx++;
323
- }
468
+ if (miniStreamSectors > 0) {
469
+ writeFatChain(miniStreamStart, miniStreamSectors);
470
+ }
471
+ for (let i = 0; i < regularNodes.length; i++) {
472
+ writeFatChain(regularNodes[i].startSector, regularSectorCounts[i]);
324
473
  }
325
- // --- Write Directory ---
326
- const dirFileOffset = (1 + firstDirSector) * SECTOR_SIZE;
327
- // Helper: write UTF-16LE name into directory entry
328
- const writeDirName = (off, name) => {
329
- for (let i = 0; i < name.length && i < 31; i++) {
330
- view.setUint16(off + i * 2, name.charCodeAt(i), true);
474
+ // ---------------------------------------------------------------------------
475
+ // 7. Directory entries.
476
+ // ---------------------------------------------------------------------------
477
+ const dirFileOffset = (1 + dirStart) * SECTOR_SIZE;
478
+ for (let i = 0; i < dir.length; i++) {
479
+ const node = dir[i];
480
+ const off = dirFileOffset + i * DIR_ENTRY_SIZE;
481
+ // UTF-16LE name (max 31 chars + null terminator).
482
+ const nameLen = Math.min(node.name.length, 31);
483
+ for (let j = 0; j < nameLen; j++) {
484
+ view.setUint16(off + j * 2, node.name.charCodeAt(j), true);
331
485
  }
332
- // Null terminator
333
- view.setUint16(off + name.length * 2, 0, true);
334
- // Name size in bytes (including null terminator)
335
- view.setUint16(off + 64, (name.length + 1) * 2, true);
336
- };
337
- // Root Entry (index 0)
338
- const rootOff = dirFileOffset;
339
- writeDirName(rootOff, "Root Entry");
340
- output[rootOff + 66] = 5; // type = root storage
341
- output[rootOff + 67] = 1; // color = black
342
- // Child (left/right/child SIDs) - set up a simple tree
343
- view.setUint32(rootOff + 68, 0xffffffff, true); // left sibling
344
- view.setUint32(rootOff + 72, 0xffffffff, true); // right sibling
345
- // Root entry child points to first entry (or 0xFFFFFFFF if none)
346
- if (entries.length > 0) {
347
- // Build a balanced-ish tree: use the middle entry as child
348
- const rootChild = entries.length === 1 ? 1 : Math.ceil(entries.length / 2);
349
- view.setUint32(rootOff + 76, rootChild, true);
486
+ view.setUint16(off + nameLen * 2, 0, true); // null terminator
487
+ view.setUint16(off + 64, (nameLen + 1) * 2, true); // name byte length
488
+ output[off + 66] = node.type; // object type
489
+ output[off + 67] = 1; // color = black
490
+ view.setUint32(off + 68, node.left, true); // left sibling
491
+ view.setUint32(off + 72, node.right, true); // right sibling
492
+ view.setUint32(off + 76, node.child, true); // child
493
+ // CLSID (16 bytes) left zero. State bits / timestamps left zero.
494
+ view.setUint32(off + 116, node.startSector, true);
495
+ // Size: 8 bytes in v4; in v3 the low 4 bytes hold the size and the high
496
+ // 4 bytes must be zero (already zero-initialized).
497
+ view.setUint32(off + 120, node.size, true);
350
498
  }
351
- else {
352
- view.setUint32(rootOff + 76, 0xffffffff, true);
499
+ // Pad any unused directory slots in the last directory sector with
500
+ // free/unknown entries (type 0, siblings NOSTREAM) so readers don't trip.
501
+ const dirSlots = dirSectors * (SECTOR_SIZE / DIR_ENTRY_SIZE);
502
+ for (let i = dir.length; i < dirSlots; i++) {
503
+ const off = dirFileOffset + i * DIR_ENTRY_SIZE;
504
+ view.setUint32(off + 68, NOSTREAM, true);
505
+ view.setUint32(off + 72, NOSTREAM, true);
506
+ view.setUint32(off + 76, NOSTREAM, true);
353
507
  }
354
- // Start sector = ENDOFCHAIN (no mini-stream)
355
- view.setUint32(rootOff + 116, ENDOFCHAIN, true);
356
- // Size = 0
357
- view.setUint32(rootOff + 120, 0, true);
358
- // Stream entries (index 1..N)
359
- // Build as a red-black tree: simple approach — balanced binary tree
360
- // For small entry counts (2-3), just arrange siblings
361
- const buildTree = (indices) => {
362
- // Map each entry to its left/right sibling
363
- const nodes = indices.map(idx => ({
364
- leftSib: 0xffffffff,
365
- rightSib: 0xffffffff,
366
- root: idx
367
- }));
368
- if (indices.length <= 1) {
369
- return nodes;
508
+ // ---------------------------------------------------------------------------
509
+ // 8. Mini-FAT.
510
+ // ---------------------------------------------------------------------------
511
+ if (miniFatSectors > 0) {
512
+ const mfOffset = (1 + miniFatStart) * SECTOR_SIZE;
513
+ const mfCapacity = miniFatSectors * (SECTOR_SIZE / 4);
514
+ const mfView = new DataView(output.buffer, mfOffset, miniFatSectors * SECTOR_SIZE);
515
+ for (let i = 0; i < mfCapacity; i++) {
516
+ mfView.setUint32(i * 4, i < miniFat.length ? miniFat[i] : FREESECT, true);
370
517
  }
371
- // Simple sorted insertion: entry at mid is root, left half is left subtree, right half is right subtree
372
- const assignTree = (arr) => {
373
- if (arr.length === 0) {
374
- return 0xffffffff;
375
- }
376
- const mid = Math.floor(arr.length / 2);
377
- const midIdx = arr[mid];
378
- const nodeEntry = nodes.find(n => n.root === midIdx);
379
- nodeEntry.leftSib = assignTree(arr.slice(0, mid));
380
- nodeEntry.rightSib = assignTree(arr.slice(mid + 1));
381
- return midIdx;
382
- };
383
- assignTree(indices);
384
- return nodes;
385
- };
386
- const dirIndices = entries.map((_, i) => i + 1); // 1-based directory indices
387
- const treeNodes = buildTree(dirIndices);
388
- for (let e = 0; e < entries.length; e++) {
389
- const entryOff = dirFileOffset + (e + 1) * DIR_ENTRY_SIZE;
390
- writeDirName(entryOff, entries[e].name);
391
- output[entryOff + 66] = 2; // type = stream
392
- output[entryOff + 67] = 1; // color = black
393
- const node = treeNodes.find(n => n.root === e + 1);
394
- view.setUint32(entryOff + 68, node.leftSib, true); // left sibling
395
- view.setUint32(entryOff + 72, node.rightSib, true); // right sibling
396
- view.setUint32(entryOff + 76, 0xffffffff, true); // child (streams have none)
397
- // Start sector
398
- view.setUint32(entryOff + 116, entryStartSectors[e], true);
399
- // Size (32-bit for v3)
400
- view.setUint32(entryOff + 120, entries[e].data.length, true);
401
518
  }
402
- // Update root entry child to the tree root
403
- if (entries.length > 0) {
404
- const sortedIndices = dirIndices.slice();
405
- const mid = Math.floor(sortedIndices.length / 2);
406
- view.setUint32(rootOff + 76, sortedIndices[mid], true);
519
+ // ---------------------------------------------------------------------------
520
+ // 9. Mini-stream data.
521
+ // ---------------------------------------------------------------------------
522
+ if (miniStreamSectors > 0) {
523
+ output.set(miniStream, (1 + miniStreamStart) * SECTOR_SIZE);
407
524
  }
408
- // --- Write Data Sectors ---
409
- for (let e = 0; e < entries.length; e++) {
410
- const dataFileOffset = (1 + entryStartSectors[e]) * SECTOR_SIZE;
411
- output.set(entries[e].data, dataFileOffset);
525
+ // ---------------------------------------------------------------------------
526
+ // 10. Regular stream data.
527
+ // ---------------------------------------------------------------------------
528
+ for (const node of regularNodes) {
529
+ output.set(node.data, (1 + node.startSector) * SECTOR_SIZE);
412
530
  }
413
531
  return output;
414
532
  }
@@ -175,16 +175,22 @@ async function computePasswordHash(password, saltBase64, algorithm, spinCount) {
175
175
  initial.set(saltBytes, 0);
176
176
  initial.set(passwordBytes, saltBytes.length);
177
177
  let hash = new Uint8Array(await crypto.subtle.digest(algName, initial));
178
- // Iterative hashing: Hi = Hash(LE_int32(i) + Hi-1)
178
+ // Iterative hashing (ISO/IEC 29500 §18.3.1.13 / MS-OFFCRYPTO §2.3.7.1):
179
+ // Hi = Hash(Hi-1 + LE_uint32(i))
180
+ // The iterator is appended AFTER the previous hash, not prepended. Getting
181
+ // the order wrong produces a hash Word cannot reproduce, so Word treats the
182
+ // document as unprotected (offering "Start Enforcing Protection" instead of
183
+ // prompting for the password). This matches the Excel encryptor, which is
184
+ // verified interoperable with Office.
179
185
  for (let i = 0; i < spinCount; i++) {
180
186
  const iterBytes = new Uint8Array(4);
181
187
  iterBytes[0] = i & 0xff;
182
188
  iterBytes[1] = (i >> 8) & 0xff;
183
189
  iterBytes[2] = (i >> 16) & 0xff;
184
190
  iterBytes[3] = (i >> 24) & 0xff;
185
- const combined = new Uint8Array(4 + hash.length);
186
- combined.set(iterBytes, 0);
187
- combined.set(hash, 4);
191
+ const combined = new Uint8Array(hash.length + 4);
192
+ combined.set(hash, 0);
193
+ combined.set(iterBytes, hash.length);
188
194
  hash = new Uint8Array(await crypto.subtle.digest(algName, combined));
189
195
  }
190
196
  return (0, internal_utils_1.bytesToBase64)(hash);