ooxml-core 0.12.0 → 0.13.0

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.
@@ -29,7 +29,7 @@ function isOoxmlCryptoError(error) {
29
29
  return code === "password-required" || code === "incorrect-password" || code === "data-integrity";
30
30
  }
31
31
 
32
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-types.js
32
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-types.js
33
33
  var OLE_MAGIC = new Uint8Array([208, 207, 17, 224, 161, 177, 26, 225]);
34
34
  var ENDOFCHAIN = 4294967294;
35
35
  var FREESECT = 4294967295;
@@ -47,10 +47,13 @@ var Ole2ParseError = class extends Error {
47
47
  }
48
48
  };
49
49
 
50
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-read.js
50
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-read.js
51
51
  function parseOle2(buffer) {
52
52
  const data = new Uint8Array(buffer);
53
53
  const view = new DataView(buffer);
54
+ if (data.length < 512) {
55
+ throw new Ole2ParseError("Truncated OLE2 compound file header");
56
+ }
54
57
  for (let i = 0; i < OLE_MAGIC.length; i++) {
55
58
  if (data[i] !== OLE_MAGIC[i]) {
56
59
  throw new Ole2ParseError("Not a valid OLE2 compound file");
@@ -64,8 +67,15 @@ function parseOle2(buffer) {
64
67
  }
65
68
  const sectorSizePower = view.getUint16(30, true);
66
69
  const miniSectorSizePower = view.getUint16(32, true);
70
+ if (majorVersion !== 3 && majorVersion !== 4 || sectorSizePower !== (majorVersion === 3 ? 9 : 12) || miniSectorSizePower !== 6) {
71
+ throw new Ole2ParseError("Unsupported OLE2 version or sector size");
72
+ }
67
73
  const sectorSize = 1 << sectorSizePower;
68
74
  const miniSectorSize = 1 << miniSectorSizePower;
75
+ if (data.length < sectorSize || data.length % sectorSize !== 0) {
76
+ throw new Ole2ParseError("Truncated OLE2 sector");
77
+ }
78
+ const sectorCount = data.length / sectorSize - 1;
69
79
  const totalFATSectors = view.getUint32(44, true);
70
80
  const firstDirectorySector = view.getUint32(48, true);
71
81
  const miniStreamCutoff = view.getUint32(56, true);
@@ -73,6 +83,9 @@ function parseOle2(buffer) {
73
83
  const totalMiniFATSectors = view.getUint32(64, true);
74
84
  const firstDIFATSector = view.getUint32(68, true);
75
85
  const totalDIFATSectors = view.getUint32(72, true);
86
+ if (totalFATSectors > sectorCount || totalMiniFATSectors > sectorCount || totalDIFATSectors > sectorCount) {
87
+ throw new Ole2ParseError("Allocation table count exceeds file size");
88
+ }
76
89
  function sectorOffset(sector) {
77
90
  return (sector + 1) * sectorSize;
78
91
  }
@@ -91,7 +104,11 @@ function parseOle2(buffer) {
91
104
  }
92
105
  }
93
106
  let difatSector = firstDIFATSector;
107
+ const difatSeen = /* @__PURE__ */ new Set();
94
108
  for (let d = 0; d < totalDIFATSectors && difatSector <= MAXREGSECT; d++) {
109
+ if (difatSeen.has(difatSector))
110
+ throw new Ole2ParseError("Circular DIFAT chain");
111
+ difatSeen.add(difatSector);
95
112
  const difatData = readSector(difatSector);
96
113
  const difatView = new DataView(difatData.buffer, difatData.byteOffset, difatData.byteLength);
97
114
  const entriesPerSector = (sectorSize - 4) / 4;
@@ -103,11 +120,14 @@ function parseOle2(buffer) {
103
120
  }
104
121
  difatSector = difatView.getUint32(sectorSize - 4, true);
105
122
  }
123
+ if (difatSeen.size !== totalDIFATSectors || fatSectors.length !== totalFATSectors || new Set(fatSectors).size !== fatSectors.length || fatSectors.some((id) => difatSeen.has(id))) {
124
+ throw new Ole2ParseError("Invalid DIFAT allocation table");
125
+ }
106
126
  const fatEntries = [];
107
127
  for (const fatSector of fatSectors) {
108
128
  const fatData = readSector(fatSector);
109
129
  const fatView = new DataView(fatData.buffer, fatData.byteOffset, fatData.byteLength);
110
- for (let i = 0; i < sectorSize / 4; i++) {
130
+ for (let i = 0; i < sectorSize / 4 && fatEntries.length < sectorCount; i++) {
111
131
  fatEntries.push(fatView.getUint32(i * 4, true));
112
132
  }
113
133
  }
@@ -121,8 +141,12 @@ function parseOle2(buffer) {
121
141
  }
122
142
  visited.add(current);
123
143
  sectors.push(readSector(current));
124
- current = fatEntries[current] ?? ENDOFCHAIN;
144
+ if (fatEntries[current] === void 0)
145
+ throw new Ole2ParseError("Missing FAT chain entry");
146
+ current = fatEntries[current];
125
147
  }
148
+ if (current !== ENDOFCHAIN)
149
+ throw new Ole2ParseError("Invalid FAT chain terminator");
126
150
  const totalLength = sectors.length * sectorSize;
127
151
  const result = new Uint8Array(totalLength);
128
152
  let offset = 0;
@@ -133,16 +157,14 @@ function parseOle2(buffer) {
133
157
  return result;
134
158
  }
135
159
  function readStream(startSector, size) {
160
+ if (size === 0)
161
+ return new Uint8Array(0);
162
+ if (size > sectorCount * sectorSize)
163
+ throw new Ole2ParseError("Stream size exceeds file size");
136
164
  const raw = readSectorChain(startSector);
137
- return raw.subarray(0, Math.min(size, raw.length));
138
- }
139
- const miniFatEntries = [];
140
- if (firstMiniFATSector <= MAXREGSECT && totalMiniFATSectors > 0) {
141
- const miniFatRaw = readSectorChain(firstMiniFATSector);
142
- const miniFatView = new DataView(miniFatRaw.buffer, miniFatRaw.byteOffset, miniFatRaw.byteLength);
143
- for (let i = 0; i < miniFatRaw.length / 4; i++) {
144
- miniFatEntries.push(miniFatView.getUint32(i * 4, true));
145
- }
165
+ if (raw.length < size)
166
+ throw new Ole2ParseError("Truncated stream sector chain");
167
+ return raw.subarray(0, size);
146
168
  }
147
169
  const dirRaw = readSectorChain(firstDirectorySector);
148
170
  const numEntries = Math.floor(dirRaw.length / DIR_ENTRY_SIZE);
@@ -155,6 +177,9 @@ function parseOle2(buffer) {
155
177
  if (objectType === ENTRY_TYPE_EMPTY) {
156
178
  continue;
157
179
  }
180
+ if (nameLen < 2 || nameLen > 64 || nameLen % 2 !== 0 || entryView.getUint16(nameLen - 2, true) !== 0) {
181
+ throw new Ole2ParseError("Invalid directory entry name");
182
+ }
158
183
  const nameBytes = Math.max(0, nameLen - 2);
159
184
  let name = "";
160
185
  for (let j = 0; j < nameBytes; j += 2) {
@@ -168,8 +193,11 @@ function parseOle2(buffer) {
168
193
  const sizeLow = entryView.getUint32(120, true);
169
194
  let size = sizeLow;
170
195
  if (majorVersion === 4) {
171
- entryView.getUint32(124, true);
172
- size = sizeLow;
196
+ const sizeHigh = entryView.getUint32(124, true);
197
+ size = sizeLow + sizeHigh * 4294967296;
198
+ if (!Number.isSafeInteger(size) || size > sectorCount * sectorSize) {
199
+ throw new Ole2ParseError("Stream size exceeds supported file bounds");
200
+ }
173
201
  }
174
202
  entries.push({
175
203
  name,
@@ -183,11 +211,28 @@ function parseOle2(buffer) {
183
211
  });
184
212
  }
185
213
  const rootEntry = entries.find((e) => e.type === ENTRY_TYPE_ROOT);
214
+ if (!rootEntry || entries.filter((e) => e.type === ENTRY_TYPE_ROOT).length !== 1) {
215
+ throw new Ole2ParseError("Invalid root directory entry");
216
+ }
186
217
  let miniStreamData;
187
218
  if (rootEntry && rootEntry.startSector <= MAXREGSECT) {
188
- miniStreamData = readSectorChain(rootEntry.startSector);
219
+ miniStreamData = readStream(rootEntry.startSector, rootEntry.size);
220
+ }
221
+ const miniFatEntries = [];
222
+ if (firstMiniFATSector <= MAXREGSECT && totalMiniFATSectors > 0) {
223
+ const miniFatRaw = readSectorChain(firstMiniFATSector);
224
+ if (miniFatRaw.length !== totalMiniFATSectors * sectorSize) {
225
+ throw new Ole2ParseError("Unexpected mini FAT chain length");
226
+ }
227
+ const miniFatView = new DataView(miniFatRaw.buffer, miniFatRaw.byteOffset, miniFatRaw.byteLength);
228
+ const miniCapacity = Math.floor((miniStreamData?.length ?? 0) / miniSectorSize);
229
+ for (let i = 0; i < miniFatRaw.length / 4 && i < miniCapacity; i++) {
230
+ miniFatEntries.push(miniFatView.getUint32(i * 4, true));
231
+ }
189
232
  }
190
233
  function readMiniStream(startSector, size) {
234
+ if (size === 0)
235
+ return new Uint8Array(0);
191
236
  if (!miniStreamData) {
192
237
  throw new Ole2ParseError("Mini stream container not found");
193
238
  }
@@ -200,17 +245,24 @@ function parseOle2(buffer) {
200
245
  }
201
246
  visited.add(current);
202
247
  const offset2 = current * miniSectorSize;
248
+ if (offset2 + miniSectorSize > miniStreamData.length || miniFatEntries[current] === void 0) {
249
+ throw new Ole2ParseError("Mini stream sector exceeds allocation bounds");
250
+ }
203
251
  sectors.push(miniStreamData.subarray(offset2, offset2 + miniSectorSize));
204
- current = miniFatEntries[current] ?? ENDOFCHAIN;
252
+ current = miniFatEntries[current];
205
253
  }
254
+ if (current !== ENDOFCHAIN)
255
+ throw new Ole2ParseError("Invalid mini FAT chain terminator");
206
256
  const totalLength = sectors.length * miniSectorSize;
257
+ if (totalLength < size)
258
+ throw new Ole2ParseError("Truncated mini stream sector chain");
207
259
  const result = new Uint8Array(totalLength);
208
260
  let offset = 0;
209
261
  for (const sector of sectors) {
210
262
  result.set(sector, offset);
211
263
  offset += miniSectorSize;
212
264
  }
213
- return result.subarray(0, Math.min(size, result.length));
265
+ return result.subarray(0, size);
214
266
  }
215
267
  function getStream(name) {
216
268
  const entry = entries.find((e) => (e.type === ENTRY_TYPE_STREAM || e.type === ENTRY_TYPE_ROOT) && e.name === name);
@@ -225,7 +277,7 @@ function parseOle2(buffer) {
225
277
  return { entries, getStream };
226
278
  }
227
279
 
228
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/utils/ooxml-crypto-primitives.js
280
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/utils/ooxml-crypto-primitives.js
229
281
  function getSubtle() {
230
282
  if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
231
283
  return globalThis.crypto.subtle;
@@ -1111,13 +1163,16 @@ function isEncryptedOoxmlPackage(input) {
1111
1163
  }
1112
1164
  }
1113
1165
 
1114
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-helpers.js
1166
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-helpers.js
1115
1167
  function compareDirEntryNames(a, b) {
1168
+ const ua = a.toUpperCase();
1169
+ const ub = b.toUpperCase();
1170
+ if (ua.length !== a.length || ub.length !== b.length) {
1171
+ throw new Error("CFB stream names with expanding uppercase mappings are unsupported");
1172
+ }
1116
1173
  if (a.length !== b.length) {
1117
1174
  return a.length - b.length;
1118
1175
  }
1119
- const ua = a.toUpperCase();
1120
- const ub = b.toUpperCase();
1121
1176
  for (let i = 0; i < ua.length; i++) {
1122
1177
  const diff = ua.charCodeAt(i) - ub.charCodeAt(i);
1123
1178
  if (diff !== 0) {
@@ -1134,9 +1189,27 @@ function encodeName(name) {
1134
1189
  }
1135
1190
  return bytes;
1136
1191
  }
1137
- function writeFatChain(fat, chain) {
1138
- for (let i = 0; i < chain.sectors.length; i++) {
1139
- fat[chain.sectors[i]] = i < chain.sectors.length - 1 ? chain.sectors[i + 1] : ENDOFCHAIN;
1192
+ function buildDirectoryTree(entryCount) {
1193
+ const links = /* @__PURE__ */ new Map();
1194
+ let redLevel = 0;
1195
+ for (let remaining = entryCount - 2; remaining >= 0; remaining = Math.floor(remaining / 2) - 1)
1196
+ redLevel++;
1197
+ const build = (low, high, level) => {
1198
+ if (low > high)
1199
+ return 4294967295;
1200
+ const id = Math.floor((low + high) / 2);
1201
+ links.set(id, {
1202
+ left: build(low, id - 1, level + 1),
1203
+ right: build(id + 1, high, level + 1),
1204
+ color: level === redLevel ? 0 : 1
1205
+ });
1206
+ return id;
1207
+ };
1208
+ return { root: build(1, entryCount - 1, 0), links };
1209
+ }
1210
+ function writeFatChain(fat, chain2) {
1211
+ for (let i = 0; i < chain2.sectors.length; i++) {
1212
+ fat[chain2.sectors[i]] = i < chain2.sectors.length - 1 ? chain2.sectors[i + 1] : ENDOFCHAIN;
1140
1213
  }
1141
1214
  }
1142
1215
  function writeFatRun(fat, firstSector, count) {
@@ -1157,7 +1230,7 @@ function writeInt32Sectors(outBytes, int32Data, firstSector, numSectors, sectorS
1157
1230
  }
1158
1231
  }
1159
1232
 
1160
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-serialize.js
1233
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-serialize.js
1161
1234
  function writeHeader(outView, outBytes, params) {
1162
1235
  outBytes.set(OLE_MAGIC, 0);
1163
1236
  outView.setUint16(24, 62, true);
@@ -1215,6 +1288,7 @@ function writeDifatSectors(outView, params) {
1215
1288
  function serializeDirectoryEntries(dirEntries, numDirSectors, sectorSize) {
1216
1289
  const dirData = new Uint8Array(numDirSectors * sectorSize);
1217
1290
  const dirView = new DataView(dirData.buffer);
1291
+ const tree = buildDirectoryTree(dirEntries.length);
1218
1292
  for (let i = 0; i < dirEntries.length; i++) {
1219
1293
  const entry = dirEntries[i];
1220
1294
  const entryOffset = i * DIR_ENTRY_SIZE;
@@ -1222,17 +1296,17 @@ function serializeDirectoryEntries(dirEntries, numDirSectors, sectorSize) {
1222
1296
  dirData.set(nameBytes.subarray(0, Math.min(nameBytes.length, 64)), entryOffset);
1223
1297
  dirView.setUint16(entryOffset + 64, Math.min((entry.name.length + 1) * 2, 64), true);
1224
1298
  dirData[entryOffset + 66] = entry.type;
1225
- dirData[entryOffset + 67] = 1;
1299
+ dirData[entryOffset + 67] = i === 0 ? 1 : tree.links.get(i).color;
1226
1300
  if (entry.clsid) {
1227
1301
  dirData.set(entry.clsid.subarray(0, 16), entryOffset + 80);
1228
1302
  }
1229
1303
  if (i === 0) {
1230
1304
  dirView.setUint32(entryOffset + 68, 4294967295, true);
1231
1305
  dirView.setUint32(entryOffset + 72, 4294967295, true);
1232
- dirView.setUint32(entryOffset + 76, dirEntries.length > 1 ? 1 : 4294967295, true);
1306
+ dirView.setUint32(entryOffset + 76, tree.root, true);
1233
1307
  } else {
1234
- dirView.setUint32(entryOffset + 68, 4294967295, true);
1235
- dirView.setUint32(entryOffset + 72, i + 1 < dirEntries.length ? i + 1 : 4294967295, true);
1308
+ dirView.setUint32(entryOffset + 68, tree.links.get(i).left, true);
1309
+ dirView.setUint32(entryOffset + 72, tree.links.get(i).right, true);
1236
1310
  dirView.setUint32(entryOffset + 76, 4294967295, true);
1237
1311
  }
1238
1312
  dirView.setUint32(entryOffset + 116, entry.startSector, true);
@@ -1240,19 +1314,32 @@ function serializeDirectoryEntries(dirEntries, numDirSectors, sectorSize) {
1240
1314
  }
1241
1315
  return dirData;
1242
1316
  }
1243
- function writeStreamSectors(outBytes, streamData, chain, sectorSize) {
1244
- for (let i = 0; i < chain.sectors.length; i++) {
1245
- const sectorOffset = (chain.sectors[i] + 1) * sectorSize;
1317
+ function writeStreamSectors(outBytes, streamData, chain2, sectorSize) {
1318
+ for (let i = 0; i < chain2.sectors.length; i++) {
1319
+ const sectorOffset = (chain2.sectors[i] + 1) * sectorSize;
1246
1320
  const srcOffset = i * sectorSize;
1247
1321
  const srcEnd = Math.min(srcOffset + sectorSize, streamData.length);
1248
1322
  outBytes.set(streamData.subarray(srcOffset, srcEnd), sectorOffset);
1249
1323
  }
1250
1324
  }
1251
1325
 
1252
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write.js
1326
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write.js
1253
1327
  function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1254
1328
  const sectorSize = 512;
1255
1329
  const miniSectorSize = 64;
1330
+ const names = [...streams2.keys()].sort(compareDirEntryNames);
1331
+ for (let i = 0; i < names.length; i++) {
1332
+ const name = names[i];
1333
+ if (name.toUpperCase().length !== name.length) {
1334
+ throw new Error("CFB stream names with expanding uppercase mappings are unsupported");
1335
+ }
1336
+ if (!name.length || name.length > 31 || /[\x00\\/:!]/.test(name)) {
1337
+ throw new Error("CFB stream names must be 1-31 UTF-16 code units without NUL, \\, /, :, or !");
1338
+ }
1339
+ if (i > 0 && compareDirEntryNames(names[i - 1], name) === 0) {
1340
+ throw new Error("Duplicate CFB stream names");
1341
+ }
1342
+ }
1256
1343
  const regularStreams = [];
1257
1344
  const miniStreams = [];
1258
1345
  for (const [name, data] of streams2) {
@@ -1270,7 +1357,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1270
1357
  for (let i = 0; i < numSectors; i++) {
1271
1358
  sectors.push(nextSector++);
1272
1359
  }
1273
- fatChains.set(stream.name, { start: sectors[0] ?? 0, sectors });
1360
+ fatChains.set(stream.name, { start: sectors[0] ?? ENDOFCHAIN, sectors });
1274
1361
  }
1275
1362
  let miniStreamContainer = new Uint8Array(0);
1276
1363
  const miniFatChains = /* @__PURE__ */ new Map();
@@ -1293,7 +1380,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1293
1380
  miniOffset += miniSectorSize;
1294
1381
  }
1295
1382
  miniFatChains.set(stream.name, {
1296
- start: miniSectors[0] ?? 0,
1383
+ start: miniSectors[0] ?? ENDOFCHAIN,
1297
1384
  sectors: miniSectors
1298
1385
  });
1299
1386
  }
@@ -1316,20 +1403,20 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1316
1403
  clsid: rootClsid
1317
1404
  });
1318
1405
  for (const stream of regularStreams) {
1319
- const chain = fatChains.get(stream.name);
1406
+ const chain2 = fatChains.get(stream.name);
1320
1407
  dirEntries.push({
1321
1408
  name: stream.name,
1322
1409
  type: ENTRY_TYPE_STREAM,
1323
- startSector: chain.start,
1410
+ startSector: chain2.start,
1324
1411
  size: stream.data.length
1325
1412
  });
1326
1413
  }
1327
1414
  for (const stream of miniStreams) {
1328
- const chain = miniFatChains.get(stream.name);
1415
+ const chain2 = miniFatChains.get(stream.name);
1329
1416
  dirEntries.push({
1330
1417
  name: stream.name,
1331
1418
  type: ENTRY_TYPE_STREAM,
1332
- startSector: chain.start,
1419
+ startSector: chain2.start,
1333
1420
  size: stream.data.length
1334
1421
  });
1335
1422
  }
@@ -1341,7 +1428,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1341
1428
  nextSector += numDirSectors;
1342
1429
  let firstMiniFATSector = ENDOFCHAIN;
1343
1430
  let numMiniFATSectors = 0;
1344
- if (miniStreams.length > 0) {
1431
+ if (nextMiniSector > 0) {
1345
1432
  numMiniFATSectors = Math.ceil(nextMiniSector * 4 / sectorSize);
1346
1433
  firstMiniFATSector = nextSector;
1347
1434
  nextSector += numMiniFATSectors;
@@ -1354,8 +1441,8 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1354
1441
  const totalSectors = nextSector;
1355
1442
  const fat = new Int32Array(numFATSectors * (sectorSize / 4));
1356
1443
  fat.fill(-1);
1357
- for (const [, chain] of fatChains) {
1358
- writeFatChain(fat, chain);
1444
+ for (const [, chain2] of fatChains) {
1445
+ writeFatChain(fat, chain2);
1359
1446
  }
1360
1447
  writeFatChain(fat, { start: rootSectors[0] ?? 0, sectors: rootSectors });
1361
1448
  writeFatRun(fat, firstDirSector, numDirSectors);
@@ -1372,8 +1459,8 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1372
1459
  if (miniStreams.length > 0) {
1373
1460
  miniFat = new Int32Array(numMiniFATSectors * (sectorSize / 4));
1374
1461
  miniFat.fill(-1);
1375
- for (const [, chain] of miniFatChains) {
1376
- writeFatChain(miniFat, chain);
1462
+ for (const [, chain2] of miniFatChains) {
1463
+ writeFatChain(miniFat, chain2);
1377
1464
  }
1378
1465
  }
1379
1466
  const fileSize = (totalSectors + 1) * sectorSize;
@@ -9489,7 +9576,7 @@ function createDocument() {
9489
9576
  };
9490
9577
  }
9491
9578
 
9492
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole-document-doc-fib.js
9579
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole-document-doc-fib.js
9493
9580
  var FC_LCB_INDEX = {
9494
9581
  plcfbteChpx: 12,
9495
9582
  plcfbtePapx: 13,
@@ -9503,8 +9590,13 @@ function readDocFib(wordDoc) {
9503
9590
  if (wIdent !== 42476) {
9504
9591
  throw new Error("Not a WordDocument FIB (bad wIdent)");
9505
9592
  }
9593
+ const nFibBase = view.getUint16(2, true);
9594
+ if (![193, 217, 257, 268, 274].includes(nFibBase))
9595
+ throw new Error("Unsupported Word FIB version");
9506
9596
  const flags1Offset = 10;
9507
9597
  const flags1 = view.getUint16(flags1Offset, true);
9598
+ if ((flags1 & 33024) !== 0)
9599
+ throw new Error("Encrypted or obfuscated Word document");
9508
9600
  const fWhichTblStm = flags1 >> 9 & 1;
9509
9601
  const csw = view.getUint16(32, true);
9510
9602
  const fibRgW97Offset = 34;
@@ -9513,6 +9605,21 @@ function readDocFib(wordDoc) {
9513
9605
  const fibRgLw97Offset = cslwOffset + 2;
9514
9606
  const cbRgFcLcbCountOffset = fibRgLw97Offset + cslw * 4;
9515
9607
  const fibRgFcLcbOffset = cbRgFcLcbCountOffset + 2;
9608
+ const fibRgFcLcbCount = view.getUint16(cbRgFcLcbCountOffset, true);
9609
+ if (csw !== 14 || cslw !== 22 || fibRgFcLcbOffset + fibRgFcLcbCount * 8 + 2 > wordDoc.length)
9610
+ throw new Error("Truncated Word FIB arrays");
9611
+ const cswNewOffset = fibRgFcLcbOffset + fibRgFcLcbCount * 8;
9612
+ const cswNew = view.getUint16(cswNewOffset, true);
9613
+ if (cswNewOffset + 2 + cswNew * 2 > wordDoc.length)
9614
+ throw new Error("Truncated Word FIB extension");
9615
+ const nFib = cswNew === 0 ? nFibBase : view.getUint16(cswNewOffset + 2, true);
9616
+ const expectedCounts = /* @__PURE__ */ new Map([[193, 93], [217, 108], [257, 136], [268, 164], [274, 183]]);
9617
+ if (expectedCounts.get(nFib) !== fibRgFcLcbCount || cswNew !== 0 && cswNew !== (nFib === 274 ? 5 : 2))
9618
+ throw new Error("Word FIB version and array counts disagree");
9619
+ const ccpText = view.getInt32(fibRgLw97Offset + 12, true);
9620
+ const otherStories = [4, 5, 7, 8, 9, 10].map((i) => view.getInt32(fibRgLw97Offset + i * 4, true));
9621
+ if (ccpText < 0 || otherStories.some((cp) => cp < 0))
9622
+ throw new Error("Negative Word story length");
9516
9623
  function fcLcbPair(index) {
9517
9624
  const off = fibRgFcLcbOffset + index * 8;
9518
9625
  return { fc: view.getUint32(off, true), lcb: view.getUint32(off + 4, true) };
@@ -9520,18 +9627,22 @@ function readDocFib(wordDoc) {
9520
9627
  const cbMacOffset = fibRgLw97Offset + 0;
9521
9628
  const ccpTextOffset = fibRgLw97Offset + 12;
9522
9629
  return {
9630
+ nFib,
9631
+ nFibBase,
9523
9632
  flags1Offset,
9524
9633
  flags1,
9525
9634
  tableStreamName: fWhichTblStm ? "1Table" : "0Table",
9526
9635
  cbMacOffset,
9527
9636
  cbMac: view.getUint32(cbMacOffset, true),
9528
9637
  ccpTextOffset,
9529
- ccpText: view.getInt32(ccpTextOffset, true),
9638
+ ccpText,
9639
+ ccpOtherStories: otherStories.reduce((sum, cp) => sum + cp, 0),
9530
9640
  plcfbteChpx: fcLcbPair(FC_LCB_INDEX.plcfbteChpx),
9531
9641
  plcfbtePapx: fcLcbPair(FC_LCB_INDEX.plcfbtePapx),
9532
9642
  sed: fcLcbPair(FC_LCB_INDEX.sed),
9533
9643
  clx: fcLcbPair(FC_LCB_INDEX.clx),
9534
- fibRgFcLcbOffset
9644
+ fibRgFcLcbOffset,
9645
+ fibRgFcLcbCount
9535
9646
  };
9536
9647
  }
9537
9648
  function fcLcbFieldOffset(fib, key) {
@@ -9539,6 +9650,9 @@ function fcLcbFieldOffset(fib, key) {
9539
9650
  }
9540
9651
  function readFcLcbAt(wordDoc, fib, index) {
9541
9652
  const view = new DataView(wordDoc.buffer, wordDoc.byteOffset, wordDoc.byteLength);
9653
+ const count = view.getUint16(fib.fibRgFcLcbOffset - 2, true);
9654
+ if (!Number.isInteger(index) || index < 0 || index >= count || fib.fibRgFcLcbCount !== void 0 && fib.fibRgFcLcbCount !== count)
9655
+ throw new Error("FIB fc/lcb index outside declared array");
9542
9656
  const off = fib.fibRgFcLcbOffset + index * 8;
9543
9657
  return { fc: view.getUint32(off, true), lcb: view.getUint32(off + 4, true) };
9544
9658
  }
@@ -9558,40 +9672,799 @@ function patchDocFib(wordDoc, fib, patch) {
9558
9672
  view.setUint32(papxOff + 4, patch.plcfbtePapx.lcb, true);
9559
9673
  }
9560
9674
 
9561
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole-document-doc-cfb.js
9675
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-stream-edit.js
9676
+ var END = 4294967294;
9677
+ var MAX = 4294967290;
9678
+ var NIL = 4294967295;
9679
+ var SIZE = 128;
9680
+ function chain(table, start, limit) {
9681
+ if (start === END && limit === 0)
9682
+ return [];
9683
+ const out = [], seen = /* @__PURE__ */ new Set();
9684
+ let id = start;
9685
+ while (id !== END) {
9686
+ if (id > MAX || id >= table.length || seen.has(id) || out.length >= limit)
9687
+ throw new Error("Invalid CFB chain");
9688
+ seen.add(id);
9689
+ out.push(id);
9690
+ id = table[id];
9691
+ }
9692
+ if (out.length !== limit)
9693
+ throw new Error("Unexpected CFB chain length");
9694
+ return out;
9695
+ }
9696
+ function chainToEnd(table, start, max) {
9697
+ if (start === END)
9698
+ return [];
9699
+ const out = [], seen = /* @__PURE__ */ new Set();
9700
+ let id = start;
9701
+ while (id !== END) {
9702
+ if (id > MAX || id >= table.length || seen.has(id) || out.length >= max)
9703
+ throw new Error("Invalid CFB chain");
9704
+ seen.add(id);
9705
+ out.push(id);
9706
+ id = table[id];
9707
+ }
9708
+ return out;
9709
+ }
9710
+ function processCompoundFileStream(input, path, replacement) {
9711
+ const reject = () => replacement ? input : void 0;
9712
+ try {
9713
+ if (path.length === 0 || path.some((part) => !part) || input.length < 512)
9714
+ return reject();
9715
+ const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
9716
+ const read32 = (offset2) => view.getUint32(offset2, true);
9717
+ if (input[0] !== 208 || input[1] !== 207 || input[2] !== 17 || input[3] !== 224 || input[4] !== 161 || input[5] !== 177 || input[6] !== 26 || input[7] !== 225 || view.getUint16(28, true) !== 65534)
9718
+ return reject();
9719
+ const major = view.getUint16(26, true), shift = view.getUint16(30, true), miniShift = view.getUint16(32, true);
9720
+ if (major !== 3 && major !== 4 || shift !== (major === 3 ? 9 : 12) || miniShift !== 6)
9721
+ return reject();
9722
+ const sectorSize = 1 << shift, miniSize = 1 << miniShift;
9723
+ if (input.length % sectorSize !== 0)
9724
+ return reject();
9725
+ const sectorCount = input.length / sectorSize - 1;
9726
+ const offset = (id) => (id + 1) * sectorSize;
9727
+ const sector = (id) => {
9728
+ if (id > MAX || id >= sectorCount)
9729
+ throw new Error("Invalid sector");
9730
+ return input.subarray(offset(id), offset(id) + sectorSize);
9731
+ };
9732
+ const fatCount = read32(44), difatCount = read32(72), difatStart = read32(68);
9733
+ const fatIds = [];
9734
+ for (let i = 0; i < 109 && fatIds.length < fatCount; i++) {
9735
+ const id = read32(76 + 4 * i);
9736
+ if (id <= MAX)
9737
+ fatIds.push(id);
9738
+ }
9739
+ let difat = difatStart;
9740
+ const difatIds = /* @__PURE__ */ new Set();
9741
+ for (let i = 0; i < difatCount; i++) {
9742
+ if (difatIds.has(difat) || difat > MAX)
9743
+ return reject();
9744
+ difatIds.add(difat);
9745
+ const bytes = sector(difat), dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
9746
+ for (let j = 0; j < (sectorSize - 4) / 4 && fatIds.length < fatCount; j++) {
9747
+ const id = dv.getUint32(j * 4, true);
9748
+ if (id <= MAX)
9749
+ fatIds.push(id);
9750
+ }
9751
+ difat = dv.getUint32(sectorSize - 4, true);
9752
+ }
9753
+ if (fatIds.length !== fatCount || new Set(fatIds).size !== fatIds.length || fatIds.some((id) => difatIds.has(id)))
9754
+ return reject();
9755
+ const fat = [];
9756
+ for (const id of fatIds) {
9757
+ const bytes = sector(id), dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
9758
+ for (let j = 0; j < sectorSize / 4 && fat.length < sectorCount; j++)
9759
+ fat.push(dv.getUint32(j * 4, true));
9760
+ }
9761
+ const exactChain = (table, start, bytes, unit) => chain(table, start, Math.ceil(bytes / unit));
9762
+ const dirStart = read32(48), dirSectors = chainToEnd(fat, dirStart, sectorCount);
9763
+ const dirBytes = new Uint8Array(dirSectors.length * sectorSize);
9764
+ dirSectors.forEach((id, i) => dirBytes.set(sector(id), i * sectorSize));
9765
+ const dvDir = new DataView(dirBytes.buffer);
9766
+ const entries = [];
9767
+ for (let p = 0, id = 0; p + SIZE <= dirBytes.length; p += SIZE, id++) {
9768
+ const type = dirBytes[p + 66];
9769
+ if (type === 0)
9770
+ continue;
9771
+ const nameLength = dvDir.getUint16(p + 64, true);
9772
+ if (nameLength < 2 || nameLength > 64 || nameLength % 2)
9773
+ return reject();
9774
+ let name = "";
9775
+ for (let i = 0; i < nameLength - 2; i += 2)
9776
+ name += String.fromCharCode(dvDir.getUint16(p + i, true));
9777
+ const sizeLow = dvDir.getUint32(p + 120, true), sizeHigh = dvDir.getUint32(p + 124, true);
9778
+ if (sizeHigh !== 0)
9779
+ return reject();
9780
+ entries.push({
9781
+ id,
9782
+ name,
9783
+ type,
9784
+ left: dvDir.getUint32(p + 68, true),
9785
+ right: dvDir.getUint32(p + 72, true),
9786
+ child: dvDir.getUint32(p + 76, true),
9787
+ start: dvDir.getUint32(p + 116, true),
9788
+ size: sizeLow
9789
+ });
9790
+ }
9791
+ const byId = new Map(entries.map((e) => [e.id, e]));
9792
+ const root = entries.find((e) => e.type === 5);
9793
+ if (!root)
9794
+ return reject();
9795
+ let parent = root, target;
9796
+ for (let partIndex = 0; partIndex < path.length; partIndex++) {
9797
+ const matches = [], seen = /* @__PURE__ */ new Set();
9798
+ const visit = (id) => {
9799
+ if (id === NIL)
9800
+ return;
9801
+ if (seen.has(id))
9802
+ throw new Error("Cyclic directory tree");
9803
+ seen.add(id);
9804
+ const e = byId.get(id);
9805
+ if (!e)
9806
+ throw new Error("Invalid directory ID");
9807
+ visit(e.left);
9808
+ if (e.name.toLocaleLowerCase() === path[partIndex].toLocaleLowerCase())
9809
+ matches.push(e);
9810
+ visit(e.right);
9811
+ };
9812
+ visit(parent.child);
9813
+ if (matches.length !== 1)
9814
+ return reject();
9815
+ target = matches[0];
9816
+ if (partIndex < path.length - 1) {
9817
+ if (target.type !== 1)
9818
+ return reject();
9819
+ parent = target;
9820
+ } else if (target.type !== 2 || replacement && replacement.length !== target.size)
9821
+ return reject();
9822
+ }
9823
+ if (!target)
9824
+ return reject();
9825
+ const names = entries.map((e) => e.name.toLowerCase());
9826
+ if (replacement && (names.includes("encryptioninfo") || names.includes("encryptedpackage")))
9827
+ return reject();
9828
+ for (const name of replacement ? ["book", "workbook"] : []) {
9829
+ if (!names.includes(name))
9830
+ continue;
9831
+ const workbook = readCompoundFileStream(input, [name]);
9832
+ if (!workbook)
9833
+ continue;
9834
+ for (let p = 0; p + 4 <= workbook.length; ) {
9835
+ const record = workbook[p] | workbook[p + 1] << 8;
9836
+ const length = workbook[p + 2] | workbook[p + 3] << 8;
9837
+ if (record === 47)
9838
+ return reject();
9839
+ if (p + 4 + length > workbook.length || record === 10)
9840
+ break;
9841
+ p += 4 + length;
9842
+ }
9843
+ }
9844
+ if (replacement && names.includes("worddocument")) {
9845
+ const word = readCompoundFileStream(input, ["WordDocument"]);
9846
+ if (word && word.length >= 12 && ((word[10] | word[11] << 8) & 256) !== 0)
9847
+ return reject();
9848
+ }
9849
+ const miniCutoff = read32(56), miniFatCount = read32(64), miniFatStart = read32(60);
9850
+ const miniFatSectors = miniFatCount ? chain(fat, miniFatStart, miniFatCount) : [];
9851
+ const metadataSectors = [...fatIds, ...difatIds, ...dirSectors, ...miniFatSectors];
9852
+ if (new Set(metadataSectors).size !== metadataSectors.length)
9853
+ return reject();
9854
+ const miniFat = [];
9855
+ const miniCapacity = Math.floor(Math.min(root.size, sectorCount * sectorSize) / miniSize);
9856
+ for (const id of miniFatSectors) {
9857
+ const bytes = sector(id), dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
9858
+ for (let j = 0; j < sectorSize / 4 && miniFat.length < miniCapacity; j++)
9859
+ miniFat.push(dv.getUint32(j * 4, true));
9860
+ }
9861
+ const reserved = /* @__PURE__ */ new Set([...fatIds, ...difatIds, ...dirSectors, ...miniFatSectors]);
9862
+ const regularUsed = new Set(reserved);
9863
+ const rootMini = entries.find((e) => e.type === 5);
9864
+ for (const entry of entries) {
9865
+ if (entry.type !== 2 && entry.type !== 5 || entry.size === 0)
9866
+ continue;
9867
+ if (entry.type === 2 && entry.size < miniCutoff)
9868
+ continue;
9869
+ for (const id of exactChain(fat, entry.start, entry.size, sectorSize)) {
9870
+ sector(id);
9871
+ if (regularUsed.has(id))
9872
+ return reject();
9873
+ regularUsed.add(id);
9874
+ }
9875
+ }
9876
+ const miniUsed = /* @__PURE__ */ new Set();
9877
+ for (const entry of entries) {
9878
+ if (entry.type !== 2 || entry.size === 0 || entry.size >= miniCutoff)
9879
+ continue;
9880
+ const ids = exactChain(miniFat, entry.start, entry.size, miniSize);
9881
+ for (const id of ids) {
9882
+ if ((id + 1) * miniSize > rootMini.size || miniUsed.has(id))
9883
+ return reject();
9884
+ miniUsed.add(id);
9885
+ }
9886
+ }
9887
+ if (!replacement) {
9888
+ if (target.size >= miniCutoff) {
9889
+ const ids2 = exactChain(fat, target.start, target.size, sectorSize);
9890
+ const result2 = new Uint8Array(ids2.length * sectorSize);
9891
+ ids2.forEach((id, i) => result2.set(sector(id), i * sectorSize));
9892
+ return result2.slice(0, target.size);
9893
+ }
9894
+ const ids = exactChain(miniFat, target.start, target.size, miniSize);
9895
+ const rootSectors = exactChain(fat, rootMini.start, rootMini.size, sectorSize);
9896
+ const result = new Uint8Array(ids.length * miniSize);
9897
+ ids.forEach((id, i) => {
9898
+ const start = id * miniSize;
9899
+ if (start + miniSize > rootMini.size)
9900
+ throw new Error("Mini stream out of bounds");
9901
+ const rootSector = rootSectors[Math.floor(start / sectorSize)];
9902
+ const source = sector(rootSector).subarray(start % sectorSize, start % sectorSize + miniSize);
9903
+ result.set(source, i * miniSize);
9904
+ });
9905
+ return result.slice(0, target.size);
9906
+ }
9907
+ if (replacement.length === 0)
9908
+ return input.slice();
9909
+ const output = input.slice();
9910
+ if (target.size >= miniCutoff) {
9911
+ const sectors = exactChain(fat, target.start, target.size, sectorSize);
9912
+ sectors.forEach((id) => sector(id));
9913
+ for (let i = 0; i < sectors.length; i++)
9914
+ output.set(replacement.subarray(i * sectorSize, Math.min((i + 1) * sectorSize, replacement.length)), offset(sectors[i]));
9915
+ } else {
9916
+ if (!miniFat.length)
9917
+ return reject();
9918
+ const miniSectors = exactChain(miniFat, target.start, target.size, miniSize);
9919
+ const rootMini2 = entries.find((e) => e.type === 5);
9920
+ const rootSectors = exactChain(fat, rootMini2.start, rootMini2.size, sectorSize);
9921
+ rootSectors.forEach((id) => sector(id));
9922
+ const rootData = new Uint8Array(rootMini2.size);
9923
+ rootSectors.forEach((id, i) => rootData.set(sector(id).subarray(0, Math.min(sectorSize, rootData.length - i * sectorSize)), i * sectorSize));
9924
+ for (let i = 0; i < replacement.length; i++) {
9925
+ const miniOffset = miniSectors[Math.floor(i / miniSize)] * miniSize + i % miniSize;
9926
+ if (miniOffset >= rootData.length)
9927
+ return reject();
9928
+ const rootSectorIndex = Math.floor(miniOffset / sectorSize);
9929
+ const rootSector = rootSectors[rootSectorIndex];
9930
+ if (rootSector === void 0)
9931
+ return reject();
9932
+ const physical = offset(rootSector) + miniOffset % sectorSize;
9933
+ output[physical] = replacement[i];
9934
+ }
9935
+ }
9936
+ return output;
9937
+ } catch {
9938
+ return reject();
9939
+ }
9940
+ }
9941
+ function readCompoundFileStream(input, path) {
9942
+ return processCompoundFileStream(input, path);
9943
+ }
9944
+ function replaceCompoundFileStream(input, path, replacement) {
9945
+ return processCompoundFileStream(input, path, replacement) ?? input;
9946
+ }
9947
+
9948
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-stream-resize-v4.js
9949
+ var END2 = 4294967294;
9950
+ var FREE = 4294967295;
9951
+ var FAT = 4294967293;
9952
+ function follow(table, start) {
9953
+ const ids = [], seen = /* @__PURE__ */ new Set();
9954
+ for (let id = start; id !== END2; id = table[id]) {
9955
+ if (id >= table.length || seen.has(id))
9956
+ throw new Error("Invalid allocation chain");
9957
+ seen.add(id);
9958
+ ids.push(id);
9959
+ }
9960
+ return ids;
9961
+ }
9962
+ function findSlot(slots, path) {
9963
+ let parent = slots.get(0);
9964
+ if (!parent || parent.type !== 5)
9965
+ throw new Error("Invalid root");
9966
+ const parents = /* @__PURE__ */ new Set();
9967
+ for (let part = 0; part < path.length; part++) {
9968
+ if (parents.has(parent.id))
9969
+ throw new Error("Cyclic storage hierarchy");
9970
+ parents.add(parent.id);
9971
+ const stack = [parent.child], seen = /* @__PURE__ */ new Set(), matches = [];
9972
+ while (stack.length) {
9973
+ const id = stack.pop();
9974
+ if (id === FREE)
9975
+ continue;
9976
+ if (seen.has(id))
9977
+ throw new Error("Invalid directory tree");
9978
+ seen.add(id);
9979
+ const slot2 = slots.get(id);
9980
+ if (!slot2)
9981
+ throw new Error("Missing directory slot");
9982
+ if (slot2.name.toLocaleLowerCase() === path[part].toLocaleLowerCase())
9983
+ matches.push(slot2);
9984
+ stack.push(slot2.left, slot2.right);
9985
+ }
9986
+ if (matches.length !== 1)
9987
+ throw new Error("Missing or ambiguous stream");
9988
+ parent = matches[0];
9989
+ if (parent.type !== (part === path.length - 1 ? 2 : 1))
9990
+ throw new Error("Invalid storage path");
9991
+ }
9992
+ return parent;
9993
+ }
9994
+ function validateHierarchy(slots) {
9995
+ const owners = /* @__PURE__ */ new Set(), storages = [0];
9996
+ while (storages.length) {
9997
+ const storage = slots.get(storages.pop());
9998
+ const stack = [storage.child], names = /* @__PURE__ */ new Set();
9999
+ while (stack.length) {
10000
+ const id = stack.pop();
10001
+ if (id === FREE)
10002
+ continue;
10003
+ const slot2 = slots.get(id);
10004
+ if (!slot2 || slot2.type !== 1 && slot2.type !== 2 || owners.has(id))
10005
+ throw new Error("Ambiguous directory ownership");
10006
+ const name = slot2.name.toLocaleLowerCase();
10007
+ if (names.has(name))
10008
+ throw new Error("Duplicate sibling name");
10009
+ names.add(name);
10010
+ owners.add(id);
10011
+ stack.push(slot2.left, slot2.right);
10012
+ if (slot2.type === 1)
10013
+ storages.push(id);
10014
+ }
10015
+ }
10016
+ if (owners.size !== slots.size - 1)
10017
+ throw new Error("Unreachable directory entries");
10018
+ }
10019
+ function resizeCompoundFileStreamV4(input, path, replacement) {
10020
+ const fail = (reason) => ({ ok: false, bytes: input, reason });
10021
+ try {
10022
+ if (input.length < 512 || !path.length || path.some((part) => !part))
10023
+ return fail("invalid-or-missing-stream");
10024
+ const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
10025
+ const read = (offset) => view.getUint32(offset, true);
10026
+ const version = view.getUint16(26, true);
10027
+ if (version !== 4)
10028
+ return fail("unsupported-version");
10029
+ const SECTOR3 = 4096, FAT_ENTRIES2 = SECTOR3 / 4, DIRECTORY_ENTRIES = SECTOR3 / 128;
10030
+ if (read(72) !== 0 || read(44) > 109)
10031
+ return fail("unsupported-difat");
10032
+ const cutoff = read(56);
10033
+ if (cutoff !== 0 && cutoff !== 4096)
10034
+ return fail("unsupported-layout");
10035
+ const original = readCompoundFileStream(input, path);
10036
+ if (!original)
10037
+ return fail("invalid-or-missing-stream");
10038
+ const name = path[path.length - 1].toLocaleLowerCase();
10039
+ if (name === "worddocument" && original.length >= 12 && (original[10] | original[11] << 8) & 33024)
10040
+ return fail("unsafe-edit");
10041
+ if (name === "book" || name === "workbook") {
10042
+ for (let offset = 0; offset + 4 <= original.length; ) {
10043
+ const record = original[offset] | original[offset + 1] << 8;
10044
+ const length = original[offset + 2] | original[offset + 3] << 8;
10045
+ if (record === 47)
10046
+ return fail("unsafe-edit");
10047
+ if (record === 10 || offset + 4 + length > original.length)
10048
+ break;
10049
+ offset += 4 + length;
10050
+ }
10051
+ }
10052
+ const validated = replaceCompoundFileStream(input, path, original);
10053
+ if (validated === input)
10054
+ return fail("unsafe-edit");
10055
+ const sameLength = replacement.length === original.length;
10056
+ if (!sameLength && original.length < cutoff)
10057
+ return fail("unsupported-mini-stream");
10058
+ if (!sameLength && replacement.length < cutoff)
10059
+ return fail("unsupported-mini-transition");
10060
+ const sectorCount = input.length / SECTOR3 - 1, fatCount = read(44);
10061
+ if (sectorCount > fatCount * FAT_ENTRIES2)
10062
+ return fail("unsafe-edit");
10063
+ const fatIds = Array.from({ length: fatCount }, (_, i) => read(76 + 4 * i));
10064
+ const fat = [];
10065
+ for (const id of fatIds) {
10066
+ for (let i = 0; i < FAT_ENTRIES2 && fat.length < sectorCount; i++)
10067
+ fat.push(read((id + 1) * SECTOR3 + i * 4));
10068
+ }
10069
+ if (fatIds.some((id) => fat[id] !== FAT))
10070
+ return fail("unsafe-edit");
10071
+ const slots = /* @__PURE__ */ new Map();
10072
+ const directoryChain = follow(fat, read(48));
10073
+ if (read(40) !== directoryChain.length)
10074
+ return fail("unsafe-edit");
10075
+ for (const [block, sector] of directoryChain.entries()) {
10076
+ for (let entry = 0; entry < DIRECTORY_ENTRIES; entry++) {
10077
+ const offset = (sector + 1) * SECTOR3 + entry * 128, type = view.getUint8(offset + 66);
10078
+ if (!type)
10079
+ continue;
10080
+ const nameLength = view.getUint16(offset + 64, true);
10081
+ if (![1, 2, 5].includes(type) || nameLength < 2 || nameLength > 64 || nameLength % 2 || view.getUint16(offset + nameLength - 2, true))
10082
+ return fail("unsafe-edit");
10083
+ let name2 = "";
10084
+ for (let i = 0; i < nameLength - 2; i += 2)
10085
+ name2 += String.fromCharCode(view.getUint16(offset + i, true));
10086
+ if (read(offset + 124) !== 0)
10087
+ return fail("unsafe-edit");
10088
+ const id = block * DIRECTORY_ENTRIES + entry;
10089
+ slots.set(id, { id, name: name2, type, left: read(offset + 68), right: read(offset + 72), child: read(offset + 76), start: read(offset + 116), size: read(offset + 120), offset });
10090
+ }
10091
+ }
10092
+ if ([...slots.values()].filter((slot2) => slot2.type === 5).length !== 1)
10093
+ return fail("unsafe-edit");
10094
+ const target = findSlot(slots, path);
10095
+ validateHierarchy(slots);
10096
+ if (sameLength) {
10097
+ const bytes = replaceCompoundFileStream(input, path, replacement);
10098
+ return bytes === input ? fail("unsafe-edit") : { ok: true, bytes };
10099
+ }
10100
+ const oldChain = follow(fat, target.start);
10101
+ if (target.size !== original.length || oldChain.length !== Math.ceil(target.size / SECTOR3))
10102
+ return fail("unsafe-edit");
10103
+ const payloadSectors = Math.ceil(replacement.length / SECTOR3);
10104
+ let newFatCount = fatCount;
10105
+ while (sectorCount + payloadSectors + newFatCount - fatCount > newFatCount * FAT_ENTRIES2)
10106
+ newFatCount++;
10107
+ if (newFatCount > 109)
10108
+ return fail("allocation-limit");
10109
+ const totalSectors = sectorCount + payloadSectors + newFatCount - fatCount;
10110
+ const output = new Uint8Array((totalSectors + 1) * SECTOR3);
10111
+ output.set(input);
10112
+ output.set(replacement, (sectorCount + 1) * SECTOR3);
10113
+ const outView = new DataView(output.buffer);
10114
+ for (let i = fatCount; i < newFatCount; i++) {
10115
+ const id = sectorCount + payloadSectors + i - fatCount;
10116
+ fatIds.push(id);
10117
+ output.fill(255, (id + 1) * SECTOR3, (id + 2) * SECTOR3);
10118
+ outView.setUint32(76 + i * 4, id, true);
10119
+ }
10120
+ outView.setUint32(44, newFatCount, true);
10121
+ const writeFat = (id, value) => outView.setUint32((fatIds[Math.floor(id / FAT_ENTRIES2)] + 1) * SECTOR3 + id % FAT_ENTRIES2 * 4, value, true);
10122
+ for (const id of oldChain)
10123
+ writeFat(id, FREE);
10124
+ for (let i = 0; i < payloadSectors; i++)
10125
+ writeFat(sectorCount + i, i + 1 === payloadSectors ? END2 : sectorCount + i + 1);
10126
+ for (const id of fatIds)
10127
+ writeFat(id, FAT);
10128
+ outView.setUint32(target.offset + 116, payloadSectors ? sectorCount : END2, true);
10129
+ outView.setUint32(target.offset + 120, replacement.length, true);
10130
+ return { ok: true, bytes: output };
10131
+ } catch {
10132
+ return fail("unsafe-edit");
10133
+ }
10134
+ }
10135
+
10136
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-stream-resize-allocation.js
10137
+ function appendCompoundAllocations(input, existingFatIds, payloads, sectorSize = 512) {
10138
+ const sectorCount = input.length / sectorSize - 1;
10139
+ const entriesPerFat = sectorSize / 4;
10140
+ const counts = payloads.map((payload) => Math.ceil(payload.length / sectorSize));
10141
+ const payloadSectors = counts.reduce((sum, count) => sum + count, 0);
10142
+ let fatCount = existingFatIds.length;
10143
+ while (sectorCount + payloadSectors + fatCount - existingFatIds.length > fatCount * entriesPerFat)
10144
+ fatCount++;
10145
+ if (fatCount > 109)
10146
+ return void 0;
10147
+ const totalSectors = sectorCount + payloadSectors + fatCount - existingFatIds.length;
10148
+ const bytes = new Uint8Array((totalSectors + 1) * sectorSize);
10149
+ bytes.set(input);
10150
+ const view = new DataView(bytes.buffer), fatIds = [...existingFatIds];
10151
+ for (let i = existingFatIds.length; i < fatCount; i++) {
10152
+ const id = sectorCount + payloadSectors + i - existingFatIds.length;
10153
+ fatIds.push(id);
10154
+ bytes.fill(255, (id + 1) * sectorSize, (id + 2) * sectorSize);
10155
+ view.setUint32(76 + i * 4, id, true);
10156
+ }
10157
+ view.setUint32(44, fatCount, true);
10158
+ const writeFat = (id, value) => view.setUint32((fatIds[Math.floor(id / entriesPerFat)] + 1) * sectorSize + id % entriesPerFat * 4, value, true);
10159
+ const chains = [];
10160
+ let next = sectorCount;
10161
+ for (let payload = 0; payload < payloads.length; payload++) {
10162
+ bytes.set(payloads[payload], (next + 1) * sectorSize);
10163
+ const chain2 = Array.from({ length: counts[payload] }, (_, i) => next + i);
10164
+ chain2.forEach((id, i) => writeFat(id, chain2[i + 1] ?? 4294967294));
10165
+ chains.push(chain2);
10166
+ next += counts[payload];
10167
+ }
10168
+ for (const id of fatIds)
10169
+ writeFat(id, 4294967293);
10170
+ return { bytes, view, chains, writeFat };
10171
+ }
10172
+ function followCompoundAllocation(table, start) {
10173
+ const ids = [], seen = /* @__PURE__ */ new Set();
10174
+ for (let id = start; id !== 4294967294; id = table[id]) {
10175
+ if (id >= table.length || seen.has(id))
10176
+ throw new Error("Invalid allocation chain");
10177
+ seen.add(id);
10178
+ ids.push(id);
10179
+ }
10180
+ return ids;
10181
+ }
10182
+
10183
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-stream-resize-mini.js
10184
+ var END3 = 4294967294;
10185
+ var FREE2 = 4294967295;
10186
+ var SECTOR = 512;
10187
+ var MINI = 64;
10188
+ function resizeMiniCompoundStream(input, fat, fatIds, root, target, replacement, cutoff) {
10189
+ const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
10190
+ const miniFatCount = inputView.getUint32(64, true);
10191
+ const miniFatSectors = miniFatCount ? followCompoundAllocation(fat, inputView.getUint32(60, true)) : [];
10192
+ if (miniFatSectors.length !== miniFatCount)
10193
+ throw new Error("Invalid mini FAT count");
10194
+ const rootSectors = root.size ? followCompoundAllocation(fat, root.start) : [];
10195
+ if (rootSectors.length !== Math.ceil(root.size / SECTOR))
10196
+ throw new Error("Invalid root mini stream");
10197
+ const miniCapacity = Math.floor(root.size / MINI), miniFat = [];
10198
+ for (const id of miniFatSectors) {
10199
+ for (let slot2 = 0; slot2 < SECTOR / 4 && miniFat.length < miniCapacity; slot2++)
10200
+ miniFat.push(inputView.getUint32((id + 1) * SECTOR + slot2 * 4, true));
10201
+ }
10202
+ const wasMini = target.size < cutoff;
10203
+ const oldChain = followCompoundAllocation(wasMini ? miniFat : fat, target.start);
10204
+ if (oldChain.length !== Math.ceil(target.size / (wasMini ? MINI : SECTOR)))
10205
+ throw new Error("Invalid stream allocation");
10206
+ if (!replacement.length) {
10207
+ const bytes = input.slice(), view = new DataView(bytes.buffer);
10208
+ for (const id of oldChain) {
10209
+ const table = wasMini ? miniFatSectors : fatIds;
10210
+ view.setUint32((table[Math.floor(id / 128)] + 1) * SECTOR + id % 128 * 4, FREE2, true);
10211
+ }
10212
+ view.setUint32(target.offset + 116, END3, true);
10213
+ view.setUint32(target.offset + 120, 0, true);
10214
+ return bytes;
10215
+ }
10216
+ if (replacement.length >= cutoff) {
10217
+ const output2 = appendCompoundAllocations(input, fatIds, [replacement]);
10218
+ if (!output2)
10219
+ return void 0;
10220
+ for (const id of oldChain)
10221
+ output2.view.setUint32((miniFatSectors[Math.floor(id / 128)] + 1) * SECTOR + id % 128 * 4, FREE2, true);
10222
+ output2.view.setUint32(target.offset + 116, output2.chains[0][0] ?? END3, true);
10223
+ output2.view.setUint32(target.offset + 120, replacement.length, true);
10224
+ return output2.bytes;
10225
+ }
10226
+ const firstMini = Math.ceil(root.size / MINI), count = Math.ceil(replacement.length / MINI);
10227
+ const newRootSize = count ? (firstMini + count) * MINI : root.size;
10228
+ const newRoot = new Uint8Array(newRootSize);
10229
+ rootSectors.forEach((id, i) => newRoot.set(input.subarray((id + 1) * SECTOR, (id + 1) * SECTOR + Math.min(SECTOR, root.size - i * SECTOR)), i * SECTOR));
10230
+ if (count)
10231
+ newRoot.set(replacement, firstMini * MINI);
10232
+ const newMiniFatCount = Math.max(miniFatCount, Math.ceil((firstMini + count) / 128));
10233
+ const newMiniFat = new Uint8Array(newMiniFatCount * SECTOR).fill(255);
10234
+ miniFatSectors.forEach((id, i) => newMiniFat.set(input.subarray((id + 1) * SECTOR, (id + 2) * SECTOR), i * SECTOR));
10235
+ const miniView = new DataView(newMiniFat.buffer);
10236
+ if (wasMini)
10237
+ for (const id of oldChain)
10238
+ miniView.setUint32(id * 4, FREE2, true);
10239
+ for (let i = 0; i < count; i++)
10240
+ miniView.setUint32((firstMini + i) * 4, i + 1 === count ? END3 : firstMini + i + 1, true);
10241
+ const output = appendCompoundAllocations(input, fatIds, [newRoot, newMiniFat]);
10242
+ if (!output)
10243
+ return void 0;
10244
+ for (const id of [...rootSectors, ...miniFatSectors, ...wasMini ? [] : oldChain])
10245
+ output.writeFat(id, FREE2);
10246
+ output.view.setUint32(root.offset + 116, output.chains[0][0] ?? END3, true);
10247
+ output.view.setUint32(root.offset + 120, newRootSize, true);
10248
+ output.view.setUint32(60, output.chains[1][0] ?? END3, true);
10249
+ output.view.setUint32(64, newMiniFatCount, true);
10250
+ output.view.setUint32(target.offset + 116, count ? firstMini : END3, true);
10251
+ output.view.setUint32(target.offset + 120, replacement.length, true);
10252
+ return output.bytes;
10253
+ }
10254
+
10255
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-stream-resize.js
10256
+ var END4 = 4294967294;
10257
+ var FREE3 = 4294967295;
10258
+ var FAT2 = 4294967293;
10259
+ var SECTOR2 = 512;
10260
+ var FAT_ENTRIES = SECTOR2 / 4;
10261
+ function follow2(table, start) {
10262
+ const ids = [], seen = /* @__PURE__ */ new Set();
10263
+ for (let id = start; id !== END4; id = table[id]) {
10264
+ if (id >= table.length || seen.has(id))
10265
+ throw new Error("Invalid allocation chain");
10266
+ seen.add(id);
10267
+ ids.push(id);
10268
+ }
10269
+ return ids;
10270
+ }
10271
+ function findSlot2(slots, path) {
10272
+ let parent = slots.get(0);
10273
+ if (!parent || parent.type !== 5)
10274
+ throw new Error("Invalid root");
10275
+ const parents = /* @__PURE__ */ new Set();
10276
+ for (let part = 0; part < path.length; part++) {
10277
+ if (parents.has(parent.id))
10278
+ throw new Error("Cyclic storage hierarchy");
10279
+ parents.add(parent.id);
10280
+ const stack = [parent.child], seen = /* @__PURE__ */ new Set(), matches = [];
10281
+ while (stack.length) {
10282
+ const id = stack.pop();
10283
+ if (id === FREE3)
10284
+ continue;
10285
+ if (seen.has(id))
10286
+ throw new Error("Invalid directory tree");
10287
+ seen.add(id);
10288
+ const slot2 = slots.get(id);
10289
+ if (!slot2)
10290
+ throw new Error("Missing directory slot");
10291
+ if (slot2.name.toLocaleLowerCase() === path[part].toLocaleLowerCase())
10292
+ matches.push(slot2);
10293
+ stack.push(slot2.left, slot2.right);
10294
+ }
10295
+ if (matches.length !== 1)
10296
+ throw new Error("Missing or ambiguous stream");
10297
+ parent = matches[0];
10298
+ if (parent.type !== (part === path.length - 1 ? 2 : 1))
10299
+ throw new Error("Invalid storage path");
10300
+ }
10301
+ return parent;
10302
+ }
10303
+ function validateHierarchy2(slots) {
10304
+ const owners = /* @__PURE__ */ new Set(), storages = [0];
10305
+ while (storages.length) {
10306
+ const storage = slots.get(storages.pop());
10307
+ const stack = [storage.child], names = /* @__PURE__ */ new Set();
10308
+ while (stack.length) {
10309
+ const id = stack.pop();
10310
+ if (id === FREE3)
10311
+ continue;
10312
+ const slot2 = slots.get(id);
10313
+ if (!slot2 || slot2.type !== 1 && slot2.type !== 2 || owners.has(id))
10314
+ throw new Error("Ambiguous directory ownership");
10315
+ const name = slot2.name.toLocaleLowerCase();
10316
+ if (names.has(name))
10317
+ throw new Error("Duplicate sibling name");
10318
+ names.add(name);
10319
+ owners.add(id);
10320
+ stack.push(slot2.left, slot2.right);
10321
+ if (slot2.type === 1)
10322
+ storages.push(id);
10323
+ }
10324
+ }
10325
+ if (owners.size !== slots.size - 1)
10326
+ throw new Error("Unreachable directory entries");
10327
+ }
10328
+ function resizeCompoundFileStream(input, path, replacement) {
10329
+ if (input.length >= 512 && new DataView(input.buffer, input.byteOffset, input.byteLength).getUint16(26, true) === 4)
10330
+ return resizeCompoundFileStreamV4(input, path, replacement);
10331
+ const fail = (reason) => ({ ok: false, bytes: input, reason });
10332
+ try {
10333
+ if (input.length < SECTOR2 || !path.length || path.some((part) => !part))
10334
+ return fail("invalid-or-missing-stream");
10335
+ const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
10336
+ const read = (offset) => view.getUint32(offset, true);
10337
+ if (view.getUint16(26, true) !== 3)
10338
+ return fail("unsupported-version");
10339
+ if (read(72) !== 0 || read(44) > 109)
10340
+ return fail("unsupported-difat");
10341
+ const cutoff = read(56);
10342
+ if (cutoff !== 0 && cutoff !== 4096)
10343
+ return fail("unsupported-layout");
10344
+ const original = readCompoundFileStream(input, path);
10345
+ if (!original)
10346
+ return fail("invalid-or-missing-stream");
10347
+ const name = path[path.length - 1].toLocaleLowerCase();
10348
+ if (name === "worddocument" && original.length >= 12 && (original[10] | original[11] << 8) & 33024)
10349
+ return fail("unsafe-edit");
10350
+ if (name === "book" || name === "workbook") {
10351
+ for (let offset = 0; offset + 4 <= original.length; ) {
10352
+ const record = original[offset] | original[offset + 1] << 8;
10353
+ const length = original[offset + 2] | original[offset + 3] << 8;
10354
+ if (record === 47)
10355
+ return fail("unsafe-edit");
10356
+ if (record === 10 || offset + 4 + length > original.length)
10357
+ break;
10358
+ offset += 4 + length;
10359
+ }
10360
+ }
10361
+ const validated = replaceCompoundFileStream(input, path, original);
10362
+ if (validated === input)
10363
+ return fail("unsafe-edit");
10364
+ const sameLength = replacement.length === original.length;
10365
+ const sectorCount = input.length / SECTOR2 - 1, fatCount = read(44);
10366
+ if (sectorCount > fatCount * FAT_ENTRIES)
10367
+ return fail("unsafe-edit");
10368
+ const fatIds = Array.from({ length: fatCount }, (_, i) => read(76 + 4 * i));
10369
+ const fat = [];
10370
+ for (const id of fatIds) {
10371
+ for (let i = 0; i < FAT_ENTRIES && fat.length < sectorCount; i++)
10372
+ fat.push(read((id + 1) * SECTOR2 + i * 4));
10373
+ }
10374
+ if (fatIds.some((id) => fat[id] !== FAT2))
10375
+ return fail("unsafe-edit");
10376
+ const slots = /* @__PURE__ */ new Map();
10377
+ for (const [block, sector] of follow2(fat, read(48)).entries()) {
10378
+ for (let entry = 0; entry < 4; entry++) {
10379
+ const offset = (sector + 1) * SECTOR2 + entry * 128, type = view.getUint8(offset + 66);
10380
+ if (!type)
10381
+ continue;
10382
+ const nameLength = view.getUint16(offset + 64, true);
10383
+ if (![1, 2, 5].includes(type) || nameLength < 2 || nameLength > 64 || nameLength % 2 || view.getUint16(offset + nameLength - 2, true))
10384
+ return fail("unsafe-edit");
10385
+ let name2 = "";
10386
+ for (let i = 0; i < nameLength - 2; i += 2)
10387
+ name2 += String.fromCharCode(view.getUint16(offset + i, true));
10388
+ const id = block * 4 + entry;
10389
+ slots.set(id, { id, name: name2, type, left: read(offset + 68), right: read(offset + 72), child: read(offset + 76), start: read(offset + 116), size: read(offset + 120), offset });
10390
+ }
10391
+ }
10392
+ if ([...slots.values()].filter((slot2) => slot2.type === 5).length !== 1)
10393
+ return fail("unsafe-edit");
10394
+ const target = findSlot2(slots, path);
10395
+ validateHierarchy2(slots);
10396
+ if (sameLength) {
10397
+ const bytes = replaceCompoundFileStream(input, path, replacement);
10398
+ return bytes === input ? fail("unsafe-edit") : { ok: true, bytes };
10399
+ }
10400
+ if (original.length < cutoff || replacement.length < cutoff) {
10401
+ const bytes = resizeMiniCompoundStream(input, fat, fatIds, slots.get(0), target, replacement, cutoff);
10402
+ return bytes ? { ok: true, bytes } : fail("allocation-limit");
10403
+ }
10404
+ const oldChain = follow2(fat, target.start);
10405
+ if (target.size !== original.length || oldChain.length !== Math.ceil(target.size / SECTOR2))
10406
+ return fail("unsafe-edit");
10407
+ const output = appendCompoundAllocations(input, fatIds, [replacement]);
10408
+ if (!output)
10409
+ return fail("allocation-limit");
10410
+ for (const id of oldChain)
10411
+ output.writeFat(id, FREE3);
10412
+ output.view.setUint32(target.offset + 116, output.chains[0][0] ?? END4, true);
10413
+ output.view.setUint32(target.offset + 120, replacement.length, true);
10414
+ return { ok: true, bytes: output.bytes };
10415
+ } catch {
10416
+ return fail("unsafe-edit");
10417
+ }
10418
+ }
10419
+
10420
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole-document-doc-cfb.js
10421
+ var DocCfbRewriteError = class extends Error {
10422
+ constructor(message) {
10423
+ super(message);
10424
+ this.name = "DocCfbRewriteError";
10425
+ }
10426
+ };
9562
10427
  function unwrapDocBytes(bytes) {
9563
10428
  if (bytes.length < 8 || bytes[0] !== 208 || bytes[1] !== 207 || bytes[2] !== 17 || bytes[3] !== 224 || bytes[4] !== 161 || bytes[5] !== 177 || bytes[6] !== 26 || bytes[7] !== 225) {
9564
10429
  return void 0;
9565
10430
  }
9566
10431
  try {
9567
- const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
9568
- const ole = parseOle2(buffer);
9569
- const wordDocBytes = ole.getStream("WordDocument");
10432
+ const wordDocBytes = readCompoundFileStream(bytes, ["WordDocument"]);
9570
10433
  if (!wordDocBytes) {
9571
10434
  return void 0;
9572
10435
  }
9573
10436
  const fib = readDocFib(wordDocBytes);
9574
- const tableBytes = ole.getStream(fib.tableStreamName);
10437
+ const tableBytes = readCompoundFileStream(bytes, [fib.tableStreamName]);
9575
10438
  if (!tableBytes) {
9576
10439
  return void 0;
9577
10440
  }
9578
- const rootClsid = ole.entries.find((e) => e.type === ENTRY_TYPE_ROOT)?.clsid;
10441
+ const header = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
10442
+ const cutoff = header.getUint32(56, true);
10443
+ const canRewrite = header.getUint16(26, true) === 3 && header.getUint32(72, true) === 0 && header.getUint32(44, true) <= 109 && (cutoff === 0 || cutoff === 4096) && wordDocBytes.length >= cutoff && tableBytes.length >= cutoff;
9579
10444
  return {
9580
10445
  wordDocBytes,
9581
10446
  tableStreamName: fib.tableStreamName,
9582
10447
  tableBytes,
10448
+ canRewrite,
9583
10449
  rewrap: (editedWordDocBytes, editedTableBytes) => {
9584
- const streams2 = /* @__PURE__ */ new Map();
9585
- for (const entry of ole.entries) {
9586
- if (entry.type !== ENTRY_TYPE_STREAM) {
9587
- continue;
9588
- }
9589
- const data = entry.name === "WordDocument" ? editedWordDocBytes : entry.name === fib.tableStreamName ? editedTableBytes : ole.getStream(entry.name);
9590
- if (data) {
9591
- streams2.set(entry.name, data);
9592
- }
10450
+ if (editedWordDocBytes.length === wordDocBytes.length && editedTableBytes.length === tableBytes.length) {
10451
+ const patchedWord = replaceCompoundFileStream(bytes, ["WordDocument"], editedWordDocBytes);
10452
+ if (patchedWord === bytes)
10453
+ throw new DocCfbRewriteError("Cannot safely patch WordDocument");
10454
+ if (editedTableBytes.every((value, i) => value === tableBytes[i]))
10455
+ return patchedWord;
10456
+ const patchedTable = replaceCompoundFileStream(patchedWord, [fib.tableStreamName], editedTableBytes);
10457
+ if (patchedTable === patchedWord)
10458
+ throw new DocCfbRewriteError("Cannot safely patch DOC table stream");
10459
+ return patchedTable;
9593
10460
  }
9594
- return new Uint8Array(buildOle2(streams2, rootClsid));
10461
+ const word = resizeCompoundFileStream(bytes, ["WordDocument"], editedWordDocBytes);
10462
+ if (!word.ok)
10463
+ throw new DocCfbRewriteError(`Cannot resize WordDocument: ${word.reason}`);
10464
+ const table = resizeCompoundFileStream(word.bytes, [fib.tableStreamName], editedTableBytes);
10465
+ if (!table.ok)
10466
+ throw new DocCfbRewriteError(`Cannot resize DOC table stream: ${table.reason}`);
10467
+ return table.bytes;
9595
10468
  }
9596
10469
  };
9597
10470
  } catch {
@@ -9599,14 +10472,19 @@ function unwrapDocBytes(bytes) {
9599
10472
  }
9600
10473
  }
9601
10474
 
9602
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole-document-doc-fkp.js
10475
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole-document-doc-fkp.js
9603
10476
  var PAGE_SIZE = 512;
9604
10477
  function parseBteTable(tableStream, at) {
9605
10478
  const view = new DataView(tableStream.buffer, tableStream.byteOffset, tableStream.byteLength);
9606
- const n = Math.floor((at.lcb - 4) / (4 + 4));
10479
+ if (!Number.isInteger(at.fc) || !Number.isInteger(at.lcb) || at.fc < 0 || at.lcb < 4 || (at.lcb - 4) % 8 !== 0 || at.fc > tableStream.length - at.lcb)
10480
+ throw new Error("Invalid BTE table bounds");
10481
+ const n = (at.lcb - 4) / 8;
9607
10482
  const fcs = [];
9608
10483
  for (let i = 0; i <= n; i++) {
9609
- fcs.push(view.getInt32(at.fc + i * 4, true));
10484
+ const fc = view.getInt32(at.fc + i * 4, true);
10485
+ if (fc < 0 || i > 0 && fc <= fcs[i - 1])
10486
+ throw new Error("Invalid BTE FC boundaries");
10487
+ fcs.push(fc);
9610
10488
  }
9611
10489
  const pns = [];
9612
10490
  for (let i = 0; i < n; i++) {
@@ -9627,10 +10505,17 @@ function buildBteTableBytes(table) {
9627
10505
  return bytes;
9628
10506
  }
9629
10507
  function findBteRangeIndex(table, fc) {
9630
- for (let i = 0; i < table.pns.length; i++) {
9631
- if (fc >= table.fcs[i] && fc < table.fcs[i + 1]) {
9632
- return i;
9633
- }
10508
+ let low = 0, high = table.pns.length;
10509
+ while (low < high) {
10510
+ const middle = low + Math.floor((high - low) / 2);
10511
+ if (fc < table.fcs[middle])
10512
+ high = middle;
10513
+ else if (fc >= table.fcs[middle + 1])
10514
+ low = middle + 1;
10515
+ else if (fc >= table.fcs[middle])
10516
+ return middle;
10517
+ else
10518
+ break;
9634
10519
  }
9635
10520
  throw new Error(`FC ${fc} is not covered by any BTE range`);
9636
10521
  }
@@ -9646,7 +10531,11 @@ function extractPapxBlob(wordDoc, papxBte, fc) {
9646
10531
  const rangeIndex = findBteRangeIndex(papxBte, fc);
9647
10532
  const pageOffset = papxBte.pns[rangeIndex] * PAGE_SIZE;
9648
10533
  const page = wordDoc.subarray(pageOffset, pageOffset + PAGE_SIZE);
10534
+ if (page.length !== PAGE_SIZE)
10535
+ throw new Error("Truncated PAPX page");
9649
10536
  const crun = page[PAGE_SIZE - 1];
10537
+ if (crun === 0 || (crun + 1) * 4 + crun * 13 > 511)
10538
+ throw new Error("Invalid PAPX run count");
9650
10539
  const pageView = new DataView(page.buffer, page.byteOffset, page.byteLength);
9651
10540
  const rgbxOffset = (crun + 1) * 4;
9652
10541
  for (let i = 0; i < crun; i++) {
@@ -9654,8 +10543,13 @@ function extractPapxBlob(wordDoc, papxBte, fc) {
9654
10543
  const runFcEnd = pageView.getInt32((i + 1) * 4, true);
9655
10544
  if (fc >= runFcStart && fc < runFcEnd) {
9656
10545
  const bOffsetWord = page[rgbxOffset + i * 13];
10546
+ if (bOffsetWord === 0)
10547
+ return new Uint8Array();
9657
10548
  const blobOffset = bOffsetWord * 2;
9658
- return page.slice(blobOffset, blobOffset + papxBlobLength(page, blobOffset));
10549
+ const blobLength = papxBlobLength(page, blobOffset);
10550
+ if (blobOffset < rgbxOffset + crun * 13 || !Number.isInteger(blobLength) || blobOffset + blobLength > 511)
10551
+ throw new Error("Invalid PAPX blob bounds");
10552
+ return page.slice(blobOffset, blobOffset + blobLength);
9659
10553
  }
9660
10554
  }
9661
10555
  throw new Error(`FC ${fc} is not covered by any run in PAPX page ${papxBte.pns[rangeIndex]}`);
@@ -9664,7 +10558,11 @@ function extractChpxBlob(wordDoc, chpxBte, fc) {
9664
10558
  const rangeIndex = findBteRangeIndex(chpxBte, fc);
9665
10559
  const pageOffset = chpxBte.pns[rangeIndex] * PAGE_SIZE;
9666
10560
  const page = wordDoc.subarray(pageOffset, pageOffset + PAGE_SIZE);
10561
+ if (page.length !== PAGE_SIZE)
10562
+ throw new Error("Truncated CHPX page");
9667
10563
  const crun = page[PAGE_SIZE - 1];
10564
+ if (crun === 0 || (crun + 1) * 4 + crun > 511)
10565
+ throw new Error("Invalid CHPX run count");
9668
10566
  const pageView = new DataView(page.buffer, page.byteOffset, page.byteLength);
9669
10567
  const rgbOffset = (crun + 1) * 4;
9670
10568
  for (let i = 0; i < crun; i++) {
@@ -9677,6 +10575,8 @@ function extractChpxBlob(wordDoc, chpxBte, fc) {
9677
10575
  }
9678
10576
  const blobOffset = bOffsetWord * 2;
9679
10577
  const cb = page[blobOffset];
10578
+ if (blobOffset < rgbOffset + crun || blobOffset + 1 + cb > 511)
10579
+ throw new Error("Invalid CHPX blob bounds");
9680
10580
  return page.slice(blobOffset, blobOffset + 1 + cb);
9681
10581
  }
9682
10582
  }
@@ -9688,8 +10588,12 @@ function buildSingleRunPapxPage(fcStart, fcEnd, blob) {
9688
10588
  view.setInt32(0, fcStart, true);
9689
10589
  view.setInt32(4, fcEnd, true);
9690
10590
  const blobByteOffset = 22;
9691
- page[8] = blobByteOffset / 2;
9692
- page.set(blob, blobByteOffset);
10591
+ if (blob.length > 511 - blobByteOffset)
10592
+ throw new Error("PAPX blob exceeds page capacity");
10593
+ if (blob.length > 0) {
10594
+ page[8] = blobByteOffset / 2;
10595
+ page.set(blob, blobByteOffset);
10596
+ }
9693
10597
  page[PAGE_SIZE - 1] = 1;
9694
10598
  return page;
9695
10599
  }
@@ -9700,6 +10604,8 @@ function buildSingleRunChpxPage(fcStart, fcEnd, blob) {
9700
10604
  view.setInt32(4, fcEnd, true);
9701
10605
  if (blob.length > 0) {
9702
10606
  const blobByteOffset = 10;
10607
+ if (blob.length > 511 - blobByteOffset)
10608
+ throw new Error("CHPX blob exceeds page capacity");
9703
10609
  page[8] = blobByteOffset / 2;
9704
10610
  page.set(blob, blobByteOffset);
9705
10611
  }
@@ -9707,7 +10613,7 @@ function buildSingleRunChpxPage(fcStart, fcEnd, blob) {
9707
10613
  return page;
9708
10614
  }
9709
10615
 
9710
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole-document-doc-cp1252.js
10616
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole-document-doc-cp1252.js
9711
10617
  var CP1252_HIGH_BLOCK = [
9712
10618
  8364,
9713
10619
  129,
@@ -9775,23 +10681,50 @@ function encodeCp1252(text) {
9775
10681
  return Uint8Array.from(bytes);
9776
10682
  }
9777
10683
 
9778
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole-document-doc-pieces.js
10684
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole-document-doc-pieces.js
9779
10685
  var CLXT_PRC = 1;
9780
10686
  var CLXT_PCDT = 2;
9781
- function parsePieceTable(tableStream, clx) {
10687
+ var DocResourceLimitError = class extends Error {
10688
+ constructor(message) {
10689
+ super(message);
10690
+ this.name = "DocResourceLimitError";
10691
+ }
10692
+ };
10693
+ function parseDocClx(tableStream, clx, maxPieces) {
10694
+ if (maxPieces !== void 0 && (!Number.isSafeInteger(maxPieces) || maxPieces < 0))
10695
+ throw new Error("Invalid DOC piece limit");
9782
10696
  const view = new DataView(tableStream.buffer, tableStream.byteOffset, tableStream.byteLength);
10697
+ if (!Number.isInteger(clx.fc) || !Number.isInteger(clx.lcb) || clx.fc < 0 || clx.lcb < 5 || clx.fc > tableStream.length - clx.lcb)
10698
+ throw new Error("Clx is outside the table stream");
9783
10699
  let off = clx.fc;
9784
10700
  const end = clx.fc + clx.lcb;
10701
+ let prcCount = 0;
9785
10702
  while (off < end) {
9786
10703
  const clxt = view.getUint8(off);
9787
10704
  if (clxt === CLXT_PRC) {
10705
+ if (off + 3 > end)
10706
+ throw new Error("Truncated Prc header");
9788
10707
  const cb = view.getUint16(off + 1, true);
10708
+ if (cb > end - off - 3)
10709
+ throw new Error("Prc extends beyond Clx");
9789
10710
  off += 1 + 2 + cb;
10711
+ prcCount++;
9790
10712
  continue;
9791
10713
  }
9792
10714
  if (clxt === CLXT_PCDT) {
10715
+ if (off + 5 > end)
10716
+ throw new Error("Truncated Pcdt header");
9793
10717
  const lcb = view.getInt32(off + 1, true);
9794
- return parsePlcPcd(view, off + 1 + 4, lcb);
10718
+ if (lcb < 4 || (lcb - 4) % 12 !== 0 || lcb !== end - off - 5)
10719
+ throw new Error("Invalid PlcPcd length");
10720
+ if (maxPieces !== void 0 && (lcb - 4) / 12 > maxPieces)
10721
+ throw new DocResourceLimitError("DOC piece count exceeds the processing limit");
10722
+ const pieces = parsePlcPcd(view, off + 5, lcb);
10723
+ for (const piece of pieces) {
10724
+ if ((piece.prm & 1) !== 0 && piece.prm >>> 1 >= prcCount)
10725
+ throw new Error("Prm1 references a missing Prc");
10726
+ }
10727
+ return { pieces, prcBytes: tableStream.slice(clx.fc, off) };
9795
10728
  }
9796
10729
  throw new Error(`Unrecognised Clx entry type ${clxt} at ${off}`);
9797
10730
  }
@@ -9801,7 +10734,10 @@ function parsePlcPcd(view, plcStart, lcb) {
9801
10734
  const n = Math.floor((lcb - 4) / (4 + 8));
9802
10735
  const cps = [];
9803
10736
  for (let i = 0; i <= n; i++) {
9804
- cps.push(view.getInt32(plcStart + i * 4, true));
10737
+ const cp = view.getInt32(plcStart + i * 4, true);
10738
+ if (i === 0 && cp !== 0 || i > 0 && cp <= cps[i - 1])
10739
+ throw new Error("Piece CPs must start at zero and increase");
10740
+ cps.push(cp);
9805
10741
  }
9806
10742
  const pcdStart = plcStart + (n + 1) * 4;
9807
10743
  const pieces = [];
@@ -9809,6 +10745,8 @@ function parsePlcPcd(view, plcStart, lcb) {
9809
10745
  const pOff = pcdStart + i * 8;
9810
10746
  const flagsWord = view.getUint16(pOff, true);
9811
10747
  const fcRaw = view.getUint32(pOff + 2, true);
10748
+ if ((fcRaw & 2147483648) !== 0)
10749
+ throw new Error("Reserved FcCompressed bit is set");
9812
10750
  const prm = view.getUint16(pOff + 6, true);
9813
10751
  const compressed = (fcRaw >>> 30 & 1) === 1;
9814
10752
  const fcField = fcRaw & 1073741823;
@@ -9825,6 +10763,9 @@ function parsePlcPcd(view, plcStart, lcb) {
9825
10763
  }
9826
10764
  function decodePieceText(wordDoc, piece) {
9827
10765
  const charLen = piece.cpEnd - piece.cpStart;
10766
+ const byteLen = charLen * (piece.compressed ? 1 : 2);
10767
+ if (!Number.isInteger(charLen) || charLen < 0 || !Number.isInteger(piece.fc) || piece.fc < 0 || piece.fc > wordDoc.length - byteLen)
10768
+ throw new Error("Piece text is outside WordDocument");
9828
10769
  if (piece.compressed) {
9829
10770
  return decodeCp1252(wordDoc.subarray(piece.fc, piece.fc + charLen));
9830
10771
  }
@@ -9835,8 +10776,19 @@ function decodePieceText(wordDoc, piece) {
9835
10776
  }
9836
10777
  return out;
9837
10778
  }
9838
- function decodePiecesText(wordDoc, pieces) {
9839
- return pieces.map((piece) => decodePieceText(wordDoc, piece)).join("");
10779
+ function decodePiecesText(wordDoc, pieces, maxChars) {
10780
+ if (maxChars !== void 0 && (!Number.isInteger(maxChars) || maxChars < 0))
10781
+ throw new Error("Invalid text character limit");
10782
+ const out = [];
10783
+ for (const piece of pieces) {
10784
+ const bytes = (piece.cpEnd - piece.cpStart) * (piece.compressed ? 1 : 2);
10785
+ if (piece.cpEnd < piece.cpStart || piece.fc < 0 || piece.fc > wordDoc.length - bytes)
10786
+ throw new Error("Piece text is outside WordDocument");
10787
+ const end = Math.min(piece.cpEnd, maxChars ?? piece.cpEnd);
10788
+ if (end > piece.cpStart)
10789
+ out.push(decodePieceText(wordDoc, { ...piece, cpEnd: end }));
10790
+ }
10791
+ return out.join("");
9840
10792
  }
9841
10793
  function encodePieceText(text) {
9842
10794
  const compressedBytes = encodeCp1252(text);
@@ -9874,7 +10826,7 @@ function replacePieceRange(pieces, cpStart, cpEnd, newPiece) {
9874
10826
  fc: newPiece.fc,
9875
10827
  compressed: newPiece.compressed,
9876
10828
  flagsWord: newPiece.flagsWord,
9877
- prm: 0,
10829
+ prm: newPiece.prm ?? 0,
9878
10830
  charLength: newPiece.charLength
9879
10831
  });
9880
10832
  inserted = true;
@@ -9895,7 +10847,7 @@ function replacePieceRange(pieces, cpStart, cpEnd, newPiece) {
9895
10847
  fc: newPiece.fc,
9896
10848
  compressed: newPiece.compressed,
9897
10849
  flagsWord: newPiece.flagsWord,
9898
- prm: 0,
10850
+ prm: newPiece.prm ?? 0,
9899
10851
  charLength: newPiece.charLength
9900
10852
  });
9901
10853
  }
@@ -9913,7 +10865,7 @@ function replacePieceRange(pieces, cpStart, cpEnd, newPiece) {
9913
10865
  };
9914
10866
  });
9915
10867
  }
9916
- function buildClxBytes(pieces) {
10868
+ function buildClxBytes(pieces, prcBytes = new Uint8Array()) {
9917
10869
  const n = pieces.length;
9918
10870
  const plcLcb = (n + 1) * 4 + n * 8;
9919
10871
  const bytes = new Uint8Array(1 + 4 + plcLcb);
@@ -9934,14 +10886,122 @@ function buildClxBytes(pieces) {
9934
10886
  view.setUint16(off + 6, piece.prm, true);
9935
10887
  off += 8;
9936
10888
  }
9937
- return bytes;
10889
+ const result = new Uint8Array(prcBytes.length + bytes.length);
10890
+ result.set(prcBytes);
10891
+ result.set(bytes, prcBytes.length);
10892
+ parseDocClx(result, { fc: 0, lcb: result.length });
10893
+ return result;
10894
+ }
10895
+
10896
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole-document-doc-fields.js
10897
+ var UnsupportedDocFieldEditError = class extends Error {
10898
+ constructor(message) {
10899
+ super(message);
10900
+ this.name = "UnsupportedDocFieldEditError";
10901
+ }
10902
+ };
10903
+ function checkFieldRanges(markers, editStart, editEnd, maxDepth) {
10904
+ const stack = [];
10905
+ for (const { cp, ch } of markers) {
10906
+ if (ch === 19) {
10907
+ if (stack.length >= maxDepth)
10908
+ throw new DocResourceLimitError("DOC field nesting exceeds the processing limit");
10909
+ stack.push({ start: cp, separated: false });
10910
+ } else if (ch === 20) {
10911
+ const field = stack.at(-1);
10912
+ if (!field || field.separated)
10913
+ throw new Error("Invalid DOC field separator nesting");
10914
+ field.separated = true;
10915
+ } else if (ch === 21) {
10916
+ const field = stack.pop();
10917
+ if (!field)
10918
+ throw new Error("Unmatched DOC field end");
10919
+ if (field.start < editEnd && cp + 1 > editStart)
10920
+ throw new UnsupportedDocFieldEditError("Paragraph overlaps a field code/result range");
10921
+ }
10922
+ }
10923
+ if (stack.length)
10924
+ throw new Error("Unterminated DOC field");
10925
+ }
10926
+ function* bodyMarkers(body) {
10927
+ for (let cp = 0; cp < body.length; cp++) {
10928
+ const ch = body.charCodeAt(cp);
10929
+ if (ch >= 19 && ch <= 21)
10930
+ yield { cp, ch };
10931
+ }
10932
+ }
10933
+ function shiftDocMainFieldCps(table, at, body, editStart, editEnd, delta, maxFields) {
10934
+ if (![editStart, editEnd, delta, maxFields].every(Number.isSafeInteger) || editStart < 0 || editEnd <= editStart || editEnd > body.length || maxFields < 0 || body.length + delta < 0)
10935
+ throw new Error("Invalid field edit bounds or budget");
10936
+ checkFieldRanges(bodyMarkers(body), editStart, editEnd, maxFields);
10937
+ if (at.lcb === 0)
10938
+ return void 0;
10939
+ if (!Number.isSafeInteger(at.fc) || at.fc < 0 || at.lcb < 4 || (at.lcb - 4) % 6 !== 0 || at.fc > table.length - at.lcb)
10940
+ throw new Error("Invalid main-story field PLC bounds");
10941
+ const n = (at.lcb - 4) / 6;
10942
+ if (n > maxFields)
10943
+ throw new DocResourceLimitError("DOC field count exceeds the processing limit");
10944
+ const source = table.subarray(at.fc, at.fc + at.lcb);
10945
+ const view = new DataView(source.buffer, source.byteOffset, source.byteLength);
10946
+ const markers = [];
10947
+ let previous = -1;
10948
+ for (let i = 0; i < n; i++) {
10949
+ const cp = view.getInt32(i * 4, true);
10950
+ const ch = source[(n + 1) * 4 + i * 2] & 31;
10951
+ if (cp <= previous || cp >= body.length || ![19, 20, 21].includes(ch) || body.charCodeAt(cp) !== ch)
10952
+ throw new Error("Invalid main-story field marker");
10953
+ markers.push({ cp, ch });
10954
+ previous = cp;
10955
+ }
10956
+ checkFieldRanges(markers, editStart, editEnd, maxFields);
10957
+ const sentinel = view.getInt32(n * 4, true);
10958
+ if (sentinel < 0 || sentinel <= previous)
10959
+ throw new Error("Invalid field PLC sentinel");
10960
+ const result = source.slice();
10961
+ const out = new DataView(result.buffer);
10962
+ let shiftedLast = -1;
10963
+ for (let i = 0; i < n; i++) {
10964
+ const cp = markers[i].cp;
10965
+ const shifted = cp >= editEnd ? cp + delta : cp;
10966
+ if (shifted < 0 || shifted >= body.length + delta || shifted <= shiftedLast)
10967
+ throw new Error("Field position shifts outside the main story");
10968
+ out.setInt32(i * 4, shifted, true);
10969
+ shiftedLast = shifted;
10970
+ }
10971
+ let shiftedSentinel = sentinel >= editEnd ? sentinel + delta : sentinel;
10972
+ if (shiftedSentinel > 2147483647 || shiftedSentinel <= shiftedLast)
10973
+ shiftedSentinel = sentinel;
10974
+ if (shiftedSentinel <= shiftedLast)
10975
+ throw new Error("No representable field PLC sentinel");
10976
+ out.setInt32(n * 4, shiftedSentinel, true);
10977
+ return result;
9938
10978
  }
9939
10979
 
9940
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole-document-doc-editor.js
9941
- var RISKY_PLCF_INDICES = [3, 4, 5, 16, 17, 18, 19, 20];
10980
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole-document-doc-editor.js
10981
+ var RISKY_PLCF_INDICES = [2, 3, 4, 5, 17, 18, 19, 20, 40, 41, 42, 43, 46, 47, 48, 54, 55, 56, 57, 58, 59, 75, 76, 89, 90];
9942
10982
  var RISKY_STTB_INDICES = [21, 22, 23];
10983
+ var SAFE_EXTENDED_FC_LCB_INDICES = /* @__PURE__ */ new Set([93, 94, 113, 181, 182]);
10984
+ var DEFAULT_OLE_DOC_PROCESSING_LIMITS = Object.freeze({ maxCharacters: 16777216, maxPieces: 65536, maxFieldRecords: 65536 });
10985
+ function processingLimits(limits) {
10986
+ const resolved = {
10987
+ maxCharacters: DEFAULT_OLE_DOC_PROCESSING_LIMITS.maxCharacters,
10988
+ maxPieces: DEFAULT_OLE_DOC_PROCESSING_LIMITS.maxPieces,
10989
+ maxFieldRecords: DEFAULT_OLE_DOC_PROCESSING_LIMITS.maxFieldRecords
10990
+ };
10991
+ if (!Number.isSafeInteger(resolved.maxCharacters) || resolved.maxCharacters < 0 || !Number.isSafeInteger(resolved.maxPieces) || resolved.maxPieces < 0 || !Number.isSafeInteger(resolved.maxFieldRecords) || resolved.maxFieldRecords < 0)
10992
+ throw new Error("DOC processing limits must be nonnegative safe integers");
10993
+ return resolved;
10994
+ }
9943
10995
  function hasUnsupportedFeatures(wordDoc, fib) {
10996
+ if (fib.ccpOtherStories > 0)
10997
+ return true;
10998
+ for (let index = 93; index < fib.fibRgFcLcbCount; index++) {
10999
+ if (!SAFE_EXTENDED_FC_LCB_INDICES.has(index) && readFcLcbAt(wordDoc, fib, index).lcb !== 0)
11000
+ return true;
11001
+ }
9944
11002
  for (const index of RISKY_PLCF_INDICES) {
11003
+ if (index >= fib.fibRgFcLcbCount)
11004
+ continue;
9945
11005
  if (readFcLcbAt(wordDoc, fib, index).lcb > 4) {
9946
11006
  return true;
9947
11007
  }
@@ -9974,15 +11034,30 @@ function paragraphBoundaries(bodyText) {
9974
11034
  }
9975
11035
  return boundaries;
9976
11036
  }
9977
- function readOleDocParagraphs(docBytes) {
11037
+ function encodePieceTextUtf16(text) {
11038
+ const bytes = new Uint8Array(text.length * 2);
11039
+ const view = new DataView(bytes.buffer);
11040
+ for (let i = 0; i < text.length; i++)
11041
+ view.setUint16(i * 2, text.charCodeAt(i), true);
11042
+ return bytes;
11043
+ }
11044
+ function readOleDocParagraphs(docBytes, limits) {
9978
11045
  try {
11046
+ const budget = processingLimits(limits);
9979
11047
  const cfb = unwrapDocBytes(docBytes);
9980
11048
  if (!cfb) {
9981
11049
  return void 0;
9982
11050
  }
9983
11051
  const fib = readDocFib(cfb.wordDocBytes);
9984
- const pieces = parsePieceTable(cfb.tableBytes, fib.clx);
9985
- const fullText = decodePiecesText(cfb.wordDocBytes, pieces);
11052
+ if (fib.ccpText > budget.maxCharacters)
11053
+ return void 0;
11054
+ const pieces = parseDocClx(cfb.tableBytes, fib.clx, budget.maxPieces).pieces;
11055
+ const totalCp = pieces.at(-1)?.cpEnd ?? 0;
11056
+ if (totalCp > fib.ccpText + fib.ccpOtherStories + 1)
11057
+ return void 0;
11058
+ const fullText = decodePiecesText(cfb.wordDocBytes, pieces, fib.ccpText);
11059
+ if (fullText.length < fib.ccpText)
11060
+ return void 0;
9986
11061
  const bodyText = fullText.slice(0, fib.ccpText);
9987
11062
  const boundaries = paragraphBoundaries(bodyText);
9988
11063
  let start = 0;
@@ -9999,7 +11074,9 @@ function readOleDocParagraphs(docBytes) {
9999
11074
  }
10000
11075
  function shiftSectionTableCps(tableBytes, sed, editEndCp, delta) {
10001
11076
  const view = new DataView(tableBytes.buffer, tableBytes.byteOffset, tableBytes.byteLength);
10002
- const n = Math.floor((sed.lcb - 4) / (4 + 12));
11077
+ if (sed.lcb < 4 || (sed.lcb - 4) % 16 !== 0 || sed.fc > tableBytes.length - sed.lcb)
11078
+ throw new Error("Invalid section table");
11079
+ const n = (sed.lcb - 4) / 16;
10003
11080
  for (let i = 0; i <= n; i++) {
10004
11081
  const off = sed.fc + i * 4;
10005
11082
  const cp = view.getInt32(off, true);
@@ -10008,31 +11085,101 @@ function shiftSectionTableCps(tableBytes, sed, editEndCp, delta) {
10008
11085
  }
10009
11086
  }
10010
11087
  }
10011
- function writeOleDocParagraphEdit(docBytes, paragraphIndex, text) {
11088
+ function writeOleDocParagraphEdit(docBytes, paragraphIndex, text, limits) {
11089
+ return tryWriteOleDocParagraphEdit(docBytes, paragraphIndex, text, limits).bytes;
11090
+ }
11091
+ function tryWriteOleDocParagraphEdit(docBytes, paragraphIndex, text, limits) {
11092
+ const rejected = (reason) => ({ status: "rejected", bytes: docBytes, reason });
11093
+ let budget;
11094
+ try {
11095
+ budget = processingLimits(limits);
11096
+ } catch {
11097
+ return rejected("invalid-limits");
11098
+ }
11099
+ if (text.length > budget.maxCharacters)
11100
+ return rejected("resource-limit");
11101
+ if (!Number.isInteger(paragraphIndex) || paragraphIndex < 0)
11102
+ return rejected("invalid-paragraph-index");
11103
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/u.test(text))
11104
+ return rejected("invalid-text");
10012
11105
  try {
10013
11106
  const cfb = unwrapDocBytes(docBytes);
10014
11107
  if (!cfb) {
10015
- return docBytes;
11108
+ return rejected("invalid-payload");
10016
11109
  }
10017
11110
  const fib = readDocFib(cfb.wordDocBytes);
10018
- if (hasUnsupportedFeatures(cfb.wordDocBytes, fib)) {
10019
- return docBytes;
10020
- }
10021
- const pieces = parsePieceTable(cfb.tableBytes, fib.clx);
10022
- const fullText = decodePiecesText(cfb.wordDocBytes, pieces);
11111
+ if (fib.ccpText > budget.maxCharacters)
11112
+ return rejected("resource-limit");
11113
+ const { pieces, prcBytes } = parseDocClx(cfb.tableBytes, fib.clx, budget.maxPieces);
11114
+ const totalCp = pieces.at(-1)?.cpEnd ?? 0;
11115
+ if (totalCp > fib.ccpText + fib.ccpOtherStories + 1)
11116
+ return rejected("invalid-document");
11117
+ const fullText = decodePiecesText(cfb.wordDocBytes, pieces, fib.ccpText);
11118
+ if (fullText.length < fib.ccpText)
11119
+ return rejected("invalid-document");
10023
11120
  const bodyText = fullText.slice(0, fib.ccpText);
10024
11121
  const boundaries = paragraphBoundaries(bodyText);
10025
11122
  const paragraphEndCp = boundaries[paragraphIndex];
10026
11123
  if (paragraphEndCp === void 0) {
10027
- return docBytes;
11124
+ return rejected("invalid-paragraph-index");
10028
11125
  }
10029
11126
  const paragraphStartCp = paragraphIndex === 0 ? 0 : boundaries[paragraphIndex - 1];
11127
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/u.test(bodyText.slice(paragraphStartCp, paragraphEndCp)))
11128
+ return rejected("unsupported-features");
11129
+ const sanitizedText = text.replaceAll(/[\r\n]+/gu, " ");
11130
+ if (fib.ccpText - (paragraphEndCp - paragraphStartCp) + sanitizedText.length + 1 > budget.maxCharacters)
11131
+ return rejected("resource-limit");
11132
+ if (sanitizedText.length + 1 === paragraphEndCp - paragraphStartCp && bodyText[paragraphEndCp - 1] === "\r") {
11133
+ const replacement = `${sanitizedText}\r`;
11134
+ const patched = cfb.wordDocBytes.slice();
11135
+ let canPatch = true;
11136
+ for (const piece of pieces) {
11137
+ const start = Math.max(paragraphStartCp, piece.cpStart);
11138
+ const end = Math.min(paragraphEndCp, piece.cpEnd);
11139
+ if (start >= end)
11140
+ continue;
11141
+ const part = replacement.slice(start - paragraphStartCp, end - paragraphStartCp);
11142
+ const unit = piece.compressed ? 1 : 2;
11143
+ const fc = piece.fc + (start - piece.cpStart) * unit;
11144
+ const fcEnd = fc + part.length * unit;
11145
+ if (pieces.some((other) => other !== piece && other.fc < fcEnd && other.fc + (other.cpEnd - other.cpStart) * (other.compressed ? 1 : 2) > fc)) {
11146
+ canPatch = false;
11147
+ break;
11148
+ }
11149
+ const encoded = piece.compressed ? encodeCp1252(part) : encodePieceTextUtf16(part);
11150
+ if (!encoded) {
11151
+ canPatch = false;
11152
+ break;
11153
+ }
11154
+ patched.set(encoded, fc);
11155
+ }
11156
+ if (canPatch)
11157
+ return { status: "edited", strategy: "preserved-runs", bytes: cfb.rewrap(patched, cfb.tableBytes) };
11158
+ }
11159
+ if (!cfb.canRewrite)
11160
+ return rejected("unsupported-container");
11161
+ if (hasUnsupportedFeatures(cfb.wordDocBytes, fib))
11162
+ return rejected("unsupported-features");
11163
+ const delta = sanitizedText.length + 1 - (paragraphEndCp - paragraphStartCp);
11164
+ const mainFields = readFcLcbAt(cfb.wordDocBytes, fib, 16);
11165
+ if (mainFields.lcb > 0) {
11166
+ for (let index = 0; index < fib.fibRgFcLcbCount; index++) {
11167
+ if (index === 16 || index === 87)
11168
+ continue;
11169
+ const other = readFcLcbAt(cfb.wordDocBytes, fib, index);
11170
+ if (other.lcb > 0 && other.fc < mainFields.fc + mainFields.lcb && other.fc + other.lcb > mainFields.fc)
11171
+ return rejected("invalid-document");
11172
+ }
11173
+ }
11174
+ const shiftedFields = shiftDocMainFieldCps(cfb.tableBytes, mainFields, bodyText, paragraphStartCp, paragraphEndCp, delta, budget.maxFieldRecords);
10030
11175
  const { piece: startPiece, fc: startFc } = pieceAndFcAtCp(pieces, paragraphStartCp);
11176
+ if (pieces.some((piece) => piece.cpStart < paragraphEndCp && piece.cpEnd > paragraphStartCp && piece.prm !== startPiece.prm))
11177
+ return rejected("unsupported-features");
11178
+ const { fc: paragraphMarkFc } = pieceAndFcAtCp(pieces, paragraphEndCp - 1);
10031
11179
  const papxBte = parseBteTable(cfb.tableBytes, fib.plcfbtePapx);
10032
11180
  const chpxBte = parseBteTable(cfb.tableBytes, fib.plcfbteChpx);
10033
- const papxBlob = extractPapxBlob(cfb.wordDocBytes, papxBte, startFc);
11181
+ const papxBlob = extractPapxBlob(cfb.wordDocBytes, papxBte, paragraphMarkFc);
10034
11182
  const chpxBlob = extractChpxBlob(cfb.wordDocBytes, chpxBte, startFc);
10035
- const sanitizedText = text.replaceAll(/[\r\n]+/gu, " ");
10036
11183
  const { bytes: textBytes, compressed } = encodePieceText(`${sanitizedText}\r`);
10037
11184
  const originalLen = cfb.wordDocBytes.length;
10038
11185
  const newTextFc = originalLen;
@@ -10060,15 +11207,19 @@ function writeOleDocParagraphEdit(docBytes, paragraphIndex, text) {
10060
11207
  fc: newTextFc,
10061
11208
  compressed,
10062
11209
  charLength: sanitizedText.length + 1,
10063
- flagsWord: startPiece.flagsWord
11210
+ flagsWord: startPiece.flagsWord,
11211
+ prm: startPiece.prm
10064
11212
  });
10065
- const delta = sanitizedText.length + 1 - (paragraphEndCp - paragraphStartCp);
11213
+ if (newPieces.length > budget.maxPieces)
11214
+ return rejected("resource-limit");
10066
11215
  const newCcpText = fib.ccpText + delta;
10067
- const clxBytes = buildClxBytes(newPieces);
11216
+ const clxBytes = buildClxBytes(newPieces, prcBytes);
10068
11217
  const papxBteBytes = buildBteTableBytes(newPapxBte);
10069
11218
  const chpxBteBytes = buildBteTableBytes(newChpxBte);
10070
11219
  const newTableBytes = new Uint8Array(cfb.tableBytes.length + clxBytes.length + papxBteBytes.length + chpxBteBytes.length);
10071
11220
  newTableBytes.set(cfb.tableBytes, 0);
11221
+ if (shiftedFields)
11222
+ newTableBytes.set(shiftedFields, mainFields.fc);
10072
11223
  let tableOff = cfb.tableBytes.length;
10073
11224
  const newClx = { fc: tableOff, lcb: clxBytes.length };
10074
11225
  newTableBytes.set(clxBytes, tableOff);
@@ -10086,9 +11237,15 @@ function writeOleDocParagraphEdit(docBytes, paragraphIndex, text) {
10086
11237
  plcfbteChpx: newChpxBteFcLcb,
10087
11238
  plcfbtePapx: newPapxBteFcLcb
10088
11239
  });
10089
- return cfb.rewrap(newWordDoc, newTableBytes);
10090
- } catch {
10091
- return docBytes;
11240
+ return { status: "edited", strategy: "piece-append", bytes: cfb.rewrap(newWordDoc, newTableBytes) };
11241
+ } catch (error) {
11242
+ if (error instanceof UnsupportedDocFieldEditError)
11243
+ return rejected("unsupported-features");
11244
+ if (error instanceof DocResourceLimitError)
11245
+ return rejected("resource-limit");
11246
+ if (error instanceof DocCfbRewriteError)
11247
+ return rejected("unsupported-container");
11248
+ return rejected("invalid-document");
10092
11249
  }
10093
11250
  }
10094
11251
 
@@ -10145,6 +11302,7 @@ function makeModel(paragraphs) {
10145
11302
  async function loadLegacyDoc(input) {
10146
11303
  const original = toBytes2(input);
10147
11304
  let streams2 = [];
11305
+ let wordDocument = null;
10148
11306
  try {
10149
11307
  const parsed = parseOle2(
10150
11308
  original.buffer.slice(
@@ -10153,6 +11311,7 @@ async function loadLegacyDoc(input) {
10153
11311
  )
10154
11312
  );
10155
11313
  streams2 = parsed.entries.map((entry) => entry.name);
11314
+ wordDocument = readCompoundFileStream(original, ["WordDocument"]) ?? null;
10156
11315
  } catch (error) {
10157
11316
  throw new LegacyDocError(
10158
11317
  `Invalid or truncated OLE compound file: ${error instanceof Error ? error.message : "parse failed"}`
@@ -10163,9 +11322,13 @@ async function loadLegacyDoc(input) {
10163
11322
  "This is a password-protected .docx, not a legacy .doc: open it with loadDocument and its password."
10164
11323
  );
10165
11324
  }
10166
- const cfb = unwrapDocBytes(original);
10167
- if (cfb && (readDocFib(cfb.wordDocBytes).flags1 & 1 << 8) !== 0) {
10168
- throw new LegacyDocError("Encrypted legacy Word documents are not supported.");
11325
+ if (wordDocument) {
11326
+ try {
11327
+ readDocFib(wordDocument);
11328
+ } catch (error) {
11329
+ if (error instanceof Error && error.message === "Encrypted or obfuscated Word document")
11330
+ throw new LegacyDocError("Encrypted legacy Word documents are not supported.");
11331
+ }
10169
11332
  }
10170
11333
  const paragraphs = readOleDocParagraphs(original);
10171
11334
  if (!paragraphs) {