@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.
- package/dist/browser/modules/archive/io/random-access.d.ts +1 -1
- package/dist/browser/modules/excel/workbook.browser.d.ts +1 -1
- package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
- package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/browser/modules/pdf/excel-bridge.d.ts +32 -0
- package/dist/browser/modules/pdf/excel-bridge.js +67 -1
- package/dist/browser/modules/pdf/word-bridge.d.ts +20 -15
- package/dist/browser/modules/pdf/word-bridge.js +49 -34
- package/dist/browser/modules/stream/common/consumers.d.ts +2 -1
- package/dist/browser/modules/word/advanced/diff.js +125 -13
- package/dist/browser/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/browser/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/browser/modules/word/builder/document-handle.d.ts +2 -0
- package/dist/browser/modules/word/builder/document-handle.js +14 -2
- package/dist/browser/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/browser/modules/word/builder/run-builders.d.ts +19 -2
- package/dist/browser/modules/word/builder/run-builders.js +2 -6
- package/dist/browser/modules/word/convert/odt/odt.js +6 -1
- package/dist/browser/modules/word/layout/layout-full.d.ts +12 -0
- package/dist/browser/modules/word/layout/layout-full.js +74 -9
- package/dist/browser/modules/word/layout/layout-model.d.ts +12 -0
- package/dist/browser/modules/word/query/merge.js +26 -10
- package/dist/browser/modules/word/query/split.js +68 -2
- package/dist/browser/modules/word/reader/docx-reader.js +23 -0
- package/dist/browser/modules/word/security/cfb-reader.d.ts +14 -3
- package/dist/browser/modules/word/security/cfb-reader.js +271 -153
- package/dist/browser/modules/word/security/document-protection.js +10 -4
- package/dist/browser/modules/word/security/encryption.js +194 -32
- package/dist/browser/modules/word/types.d.ts +17 -0
- package/dist/browser/modules/word/units.d.ts +10 -4
- package/dist/browser/modules/word/units.js +10 -4
- package/dist/browser/modules/word/writer/document-writer.js +28 -4
- package/dist/browser/modules/word/writer/docx-packager.js +45 -5
- package/dist/browser/modules/word/writer/image-writer.d.ts +1 -1
- package/dist/browser/modules/word/writer/image-writer.js +2 -2
- package/dist/browser/modules/word/writer/render-context.d.ts +15 -0
- package/dist/browser/modules/word/writer/run-writer.js +8 -4
- package/dist/browser/modules/word/writer/section-writer.js +46 -35
- package/dist/browser/modules/word/writer/streaming-writer.js +4 -0
- package/dist/browser/modules/word/writer/styles-writer.js +11 -0
- package/dist/browser/modules/word/writer/table-writer.js +6 -0
- package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/cjs/modules/pdf/excel-bridge.js +67 -0
- package/dist/cjs/modules/pdf/word-bridge.js +49 -34
- package/dist/cjs/modules/word/advanced/diff.js +125 -13
- package/dist/cjs/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/cjs/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/cjs/modules/word/builder/document-handle.js +14 -2
- package/dist/cjs/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/cjs/modules/word/builder/run-builders.js +2 -6
- package/dist/cjs/modules/word/convert/odt/odt.js +6 -1
- package/dist/cjs/modules/word/layout/layout-full.js +74 -9
- package/dist/cjs/modules/word/query/merge.js +26 -10
- package/dist/cjs/modules/word/query/split.js +68 -2
- package/dist/cjs/modules/word/reader/docx-reader.js +23 -0
- package/dist/cjs/modules/word/security/cfb-reader.js +271 -153
- package/dist/cjs/modules/word/security/document-protection.js +10 -4
- package/dist/cjs/modules/word/security/encryption.js +193 -31
- package/dist/cjs/modules/word/units.js +10 -4
- package/dist/cjs/modules/word/writer/document-writer.js +28 -4
- package/dist/cjs/modules/word/writer/docx-packager.js +45 -5
- package/dist/cjs/modules/word/writer/image-writer.js +2 -2
- package/dist/cjs/modules/word/writer/run-writer.js +8 -4
- package/dist/cjs/modules/word/writer/section-writer.js +46 -35
- package/dist/cjs/modules/word/writer/streaming-writer.js +4 -0
- package/dist/cjs/modules/word/writer/styles-writer.js +11 -0
- package/dist/cjs/modules/word/writer/table-writer.js +6 -0
- package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/esm/modules/pdf/excel-bridge.js +67 -1
- package/dist/esm/modules/pdf/word-bridge.js +49 -34
- package/dist/esm/modules/word/advanced/diff.js +125 -13
- package/dist/esm/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/esm/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/esm/modules/word/builder/document-handle.js +14 -2
- package/dist/esm/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/esm/modules/word/builder/run-builders.js +2 -6
- package/dist/esm/modules/word/convert/odt/odt.js +6 -1
- package/dist/esm/modules/word/layout/layout-full.js +74 -9
- package/dist/esm/modules/word/query/merge.js +26 -10
- package/dist/esm/modules/word/query/split.js +68 -2
- package/dist/esm/modules/word/reader/docx-reader.js +23 -0
- package/dist/esm/modules/word/security/cfb-reader.js +271 -153
- package/dist/esm/modules/word/security/document-protection.js +10 -4
- package/dist/esm/modules/word/security/encryption.js +194 -32
- package/dist/esm/modules/word/units.js +10 -4
- package/dist/esm/modules/word/writer/document-writer.js +28 -4
- package/dist/esm/modules/word/writer/docx-packager.js +45 -5
- package/dist/esm/modules/word/writer/image-writer.js +2 -2
- package/dist/esm/modules/word/writer/run-writer.js +8 -4
- package/dist/esm/modules/word/writer/section-writer.js +46 -35
- package/dist/esm/modules/word/writer/streaming-writer.js +4 -0
- package/dist/esm/modules/word/writer/styles-writer.js +11 -0
- package/dist/esm/modules/word/writer/table-writer.js +6 -0
- package/dist/iife/excelts.iife.js +20 -8
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +2 -2
- package/dist/types/modules/archive/io/random-access.d.ts +1 -1
- package/dist/types/modules/excel/workbook.browser.d.ts +1 -1
- package/dist/types/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
- package/dist/types/modules/pdf/excel-bridge.d.ts +32 -0
- package/dist/types/modules/pdf/word-bridge.d.ts +20 -15
- package/dist/types/modules/stream/common/consumers.d.ts +2 -1
- package/dist/types/modules/word/builder/document-handle.d.ts +2 -0
- package/dist/types/modules/word/builder/run-builders.d.ts +19 -2
- package/dist/types/modules/word/layout/layout-full.d.ts +12 -0
- package/dist/types/modules/word/layout/layout-model.d.ts +12 -0
- package/dist/types/modules/word/security/cfb-reader.d.ts +14 -3
- package/dist/types/modules/word/types.d.ts +17 -0
- package/dist/types/modules/word/units.d.ts +10 -4
- package/dist/types/modules/word/writer/image-writer.d.ts +1 -1
- package/dist/types/modules/word/writer/render-context.d.ts +15 -0
- 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
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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
|
-
//
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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 =
|
|
247
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
//
|
|
258
|
-
|
|
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
|
-
|
|
264
|
-
view.setUint16(
|
|
265
|
-
view.setUint16(
|
|
266
|
-
//
|
|
267
|
-
view.setUint16(
|
|
268
|
-
//
|
|
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
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
view.setUint32(
|
|
280
|
-
//
|
|
281
|
-
view.setUint32(
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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(
|
|
307
|
-
sectorIdx++;
|
|
462
|
+
fatView.setUint32((fatStart + i) * 4, 0xfffffffd, true);
|
|
308
463
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
fatView.setUint32(sectorIdx * 4, next, true);
|
|
313
|
-
sectorIdx++;
|
|
464
|
+
writeFatChain(dirStart, dirSectors);
|
|
465
|
+
if (miniFatSectors > 0) {
|
|
466
|
+
writeFatChain(miniFatStart, miniFatSectors);
|
|
314
467
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
//
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
//
|
|
333
|
-
view.setUint16(off +
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
//
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// 9. Mini-stream data.
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
if (miniStreamSectors > 0) {
|
|
523
|
+
output.set(miniStream, (1 + miniStreamStart) * SECTOR_SIZE);
|
|
407
524
|
}
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
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(
|
|
186
|
-
combined.set(
|
|
187
|
-
combined.set(
|
|
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);
|