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