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