@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
|
@@ -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
|
|
222
|
-
*
|
|
223
|
-
*
|
|
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
|
-
//
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 =
|
|
243
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
//
|
|
254
|
-
|
|
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
|
-
|
|
260
|
-
view.setUint16(
|
|
261
|
-
view.setUint16(
|
|
262
|
-
//
|
|
263
|
-
view.setUint16(
|
|
264
|
-
//
|
|
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
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
view.setUint32(
|
|
276
|
-
//
|
|
277
|
-
view.setUint32(
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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(
|
|
303
|
-
sectorIdx++;
|
|
458
|
+
fatView.setUint32((fatStart + i) * 4, 0xfffffffd, true);
|
|
304
459
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
fatView.setUint32(sectorIdx * 4, next, true);
|
|
309
|
-
sectorIdx++;
|
|
460
|
+
writeFatChain(dirStart, dirSectors);
|
|
461
|
+
if (miniFatSectors > 0) {
|
|
462
|
+
writeFatChain(miniFatStart, miniFatSectors);
|
|
310
463
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
//
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
//
|
|
329
|
-
view.setUint16(off +
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
//
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// 9. Mini-stream data.
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
if (miniStreamSectors > 0) {
|
|
519
|
+
output.set(miniStream, (1 + miniStreamStart) * SECTOR_SIZE);
|
|
403
520
|
}
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|
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(
|
|
179
|
-
combined.set(
|
|
180
|
-
combined.set(
|
|
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);
|