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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-types.js
3
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-types.js
4
4
  var OLE_MAGIC = new Uint8Array([208, 207, 17, 224, 161, 177, 26, 225]);
5
5
  var ENDOFCHAIN = 4294967294;
6
6
  var FREESECT = 4294967295;
@@ -18,10 +18,13 @@ var Ole2ParseError = class extends Error {
18
18
  }
19
19
  };
20
20
 
21
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-read.js
21
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-read.js
22
22
  function parseOle2(buffer) {
23
23
  const data = new Uint8Array(buffer);
24
24
  const view = new DataView(buffer);
25
+ if (data.length < 512) {
26
+ throw new Ole2ParseError("Truncated OLE2 compound file header");
27
+ }
25
28
  for (let i = 0; i < OLE_MAGIC.length; i++) {
26
29
  if (data[i] !== OLE_MAGIC[i]) {
27
30
  throw new Ole2ParseError("Not a valid OLE2 compound file");
@@ -35,8 +38,15 @@ function parseOle2(buffer) {
35
38
  }
36
39
  const sectorSizePower = view.getUint16(30, true);
37
40
  const miniSectorSizePower = view.getUint16(32, true);
41
+ if (majorVersion !== 3 && majorVersion !== 4 || sectorSizePower !== (majorVersion === 3 ? 9 : 12) || miniSectorSizePower !== 6) {
42
+ throw new Ole2ParseError("Unsupported OLE2 version or sector size");
43
+ }
38
44
  const sectorSize = 1 << sectorSizePower;
39
45
  const miniSectorSize = 1 << miniSectorSizePower;
46
+ if (data.length < sectorSize || data.length % sectorSize !== 0) {
47
+ throw new Ole2ParseError("Truncated OLE2 sector");
48
+ }
49
+ const sectorCount = data.length / sectorSize - 1;
40
50
  const totalFATSectors = view.getUint32(44, true);
41
51
  const firstDirectorySector = view.getUint32(48, true);
42
52
  const miniStreamCutoff = view.getUint32(56, true);
@@ -44,6 +54,9 @@ function parseOle2(buffer) {
44
54
  const totalMiniFATSectors = view.getUint32(64, true);
45
55
  const firstDIFATSector = view.getUint32(68, true);
46
56
  const totalDIFATSectors = view.getUint32(72, true);
57
+ if (totalFATSectors > sectorCount || totalMiniFATSectors > sectorCount || totalDIFATSectors > sectorCount) {
58
+ throw new Ole2ParseError("Allocation table count exceeds file size");
59
+ }
47
60
  function sectorOffset(sector) {
48
61
  return (sector + 1) * sectorSize;
49
62
  }
@@ -62,7 +75,11 @@ function parseOle2(buffer) {
62
75
  }
63
76
  }
64
77
  let difatSector = firstDIFATSector;
78
+ const difatSeen = /* @__PURE__ */ new Set();
65
79
  for (let d = 0; d < totalDIFATSectors && difatSector <= MAXREGSECT; d++) {
80
+ if (difatSeen.has(difatSector))
81
+ throw new Ole2ParseError("Circular DIFAT chain");
82
+ difatSeen.add(difatSector);
66
83
  const difatData = readSector(difatSector);
67
84
  const difatView = new DataView(difatData.buffer, difatData.byteOffset, difatData.byteLength);
68
85
  const entriesPerSector = (sectorSize - 4) / 4;
@@ -74,11 +91,14 @@ function parseOle2(buffer) {
74
91
  }
75
92
  difatSector = difatView.getUint32(sectorSize - 4, true);
76
93
  }
94
+ if (difatSeen.size !== totalDIFATSectors || fatSectors.length !== totalFATSectors || new Set(fatSectors).size !== fatSectors.length || fatSectors.some((id) => difatSeen.has(id))) {
95
+ throw new Ole2ParseError("Invalid DIFAT allocation table");
96
+ }
77
97
  const fatEntries = [];
78
98
  for (const fatSector of fatSectors) {
79
99
  const fatData = readSector(fatSector);
80
100
  const fatView = new DataView(fatData.buffer, fatData.byteOffset, fatData.byteLength);
81
- for (let i = 0; i < sectorSize / 4; i++) {
101
+ for (let i = 0; i < sectorSize / 4 && fatEntries.length < sectorCount; i++) {
82
102
  fatEntries.push(fatView.getUint32(i * 4, true));
83
103
  }
84
104
  }
@@ -92,8 +112,12 @@ function parseOle2(buffer) {
92
112
  }
93
113
  visited.add(current);
94
114
  sectors.push(readSector(current));
95
- current = fatEntries[current] ?? ENDOFCHAIN;
115
+ if (fatEntries[current] === void 0)
116
+ throw new Ole2ParseError("Missing FAT chain entry");
117
+ current = fatEntries[current];
96
118
  }
119
+ if (current !== ENDOFCHAIN)
120
+ throw new Ole2ParseError("Invalid FAT chain terminator");
97
121
  const totalLength = sectors.length * sectorSize;
98
122
  const result = new Uint8Array(totalLength);
99
123
  let offset = 0;
@@ -104,16 +128,14 @@ function parseOle2(buffer) {
104
128
  return result;
105
129
  }
106
130
  function readStream(startSector, size) {
131
+ if (size === 0)
132
+ return new Uint8Array(0);
133
+ if (size > sectorCount * sectorSize)
134
+ throw new Ole2ParseError("Stream size exceeds file size");
107
135
  const raw = readSectorChain(startSector);
108
- return raw.subarray(0, Math.min(size, raw.length));
109
- }
110
- const miniFatEntries = [];
111
- if (firstMiniFATSector <= MAXREGSECT && totalMiniFATSectors > 0) {
112
- const miniFatRaw = readSectorChain(firstMiniFATSector);
113
- const miniFatView = new DataView(miniFatRaw.buffer, miniFatRaw.byteOffset, miniFatRaw.byteLength);
114
- for (let i = 0; i < miniFatRaw.length / 4; i++) {
115
- miniFatEntries.push(miniFatView.getUint32(i * 4, true));
116
- }
136
+ if (raw.length < size)
137
+ throw new Ole2ParseError("Truncated stream sector chain");
138
+ return raw.subarray(0, size);
117
139
  }
118
140
  const dirRaw = readSectorChain(firstDirectorySector);
119
141
  const numEntries = Math.floor(dirRaw.length / DIR_ENTRY_SIZE);
@@ -126,6 +148,9 @@ function parseOle2(buffer) {
126
148
  if (objectType === ENTRY_TYPE_EMPTY) {
127
149
  continue;
128
150
  }
151
+ if (nameLen < 2 || nameLen > 64 || nameLen % 2 !== 0 || entryView.getUint16(nameLen - 2, true) !== 0) {
152
+ throw new Ole2ParseError("Invalid directory entry name");
153
+ }
129
154
  const nameBytes = Math.max(0, nameLen - 2);
130
155
  let name = "";
131
156
  for (let j = 0; j < nameBytes; j += 2) {
@@ -139,8 +164,11 @@ function parseOle2(buffer) {
139
164
  const sizeLow = entryView.getUint32(120, true);
140
165
  let size = sizeLow;
141
166
  if (majorVersion === 4) {
142
- entryView.getUint32(124, true);
143
- size = sizeLow;
167
+ const sizeHigh = entryView.getUint32(124, true);
168
+ size = sizeLow + sizeHigh * 4294967296;
169
+ if (!Number.isSafeInteger(size) || size > sectorCount * sectorSize) {
170
+ throw new Ole2ParseError("Stream size exceeds supported file bounds");
171
+ }
144
172
  }
145
173
  entries.push({
146
174
  name,
@@ -154,11 +182,28 @@ function parseOle2(buffer) {
154
182
  });
155
183
  }
156
184
  const rootEntry = entries.find((e) => e.type === ENTRY_TYPE_ROOT);
185
+ if (!rootEntry || entries.filter((e) => e.type === ENTRY_TYPE_ROOT).length !== 1) {
186
+ throw new Ole2ParseError("Invalid root directory entry");
187
+ }
157
188
  let miniStreamData;
158
189
  if (rootEntry && rootEntry.startSector <= MAXREGSECT) {
159
- miniStreamData = readSectorChain(rootEntry.startSector);
190
+ miniStreamData = readStream(rootEntry.startSector, rootEntry.size);
191
+ }
192
+ const miniFatEntries = [];
193
+ if (firstMiniFATSector <= MAXREGSECT && totalMiniFATSectors > 0) {
194
+ const miniFatRaw = readSectorChain(firstMiniFATSector);
195
+ if (miniFatRaw.length !== totalMiniFATSectors * sectorSize) {
196
+ throw new Ole2ParseError("Unexpected mini FAT chain length");
197
+ }
198
+ const miniFatView = new DataView(miniFatRaw.buffer, miniFatRaw.byteOffset, miniFatRaw.byteLength);
199
+ const miniCapacity = Math.floor((miniStreamData?.length ?? 0) / miniSectorSize);
200
+ for (let i = 0; i < miniFatRaw.length / 4 && i < miniCapacity; i++) {
201
+ miniFatEntries.push(miniFatView.getUint32(i * 4, true));
202
+ }
160
203
  }
161
204
  function readMiniStream(startSector, size) {
205
+ if (size === 0)
206
+ return new Uint8Array(0);
162
207
  if (!miniStreamData) {
163
208
  throw new Ole2ParseError("Mini stream container not found");
164
209
  }
@@ -171,17 +216,24 @@ function parseOle2(buffer) {
171
216
  }
172
217
  visited.add(current);
173
218
  const offset2 = current * miniSectorSize;
219
+ if (offset2 + miniSectorSize > miniStreamData.length || miniFatEntries[current] === void 0) {
220
+ throw new Ole2ParseError("Mini stream sector exceeds allocation bounds");
221
+ }
174
222
  sectors.push(miniStreamData.subarray(offset2, offset2 + miniSectorSize));
175
- current = miniFatEntries[current] ?? ENDOFCHAIN;
223
+ current = miniFatEntries[current];
176
224
  }
225
+ if (current !== ENDOFCHAIN)
226
+ throw new Ole2ParseError("Invalid mini FAT chain terminator");
177
227
  const totalLength = sectors.length * miniSectorSize;
228
+ if (totalLength < size)
229
+ throw new Ole2ParseError("Truncated mini stream sector chain");
178
230
  const result = new Uint8Array(totalLength);
179
231
  let offset = 0;
180
232
  for (const sector of sectors) {
181
233
  result.set(sector, offset);
182
234
  offset += miniSectorSize;
183
235
  }
184
- return result.subarray(0, Math.min(size, result.length));
236
+ return result.subarray(0, size);
185
237
  }
186
238
  function getStream(name) {
187
239
  const entry = entries.find((e) => (e.type === ENTRY_TYPE_STREAM || e.type === ENTRY_TYPE_ROOT) && e.name === name);
@@ -196,7 +248,7 @@ function parseOle2(buffer) {
196
248
  return { entries, getStream };
197
249
  }
198
250
 
199
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/utils/ooxml-crypto-primitives.js
251
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/utils/ooxml-crypto-primitives.js
200
252
  function getSubtle() {
201
253
  if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
202
254
  return globalThis.crypto.subtle;
@@ -1128,13 +1180,16 @@ var EncryptedFileError = class extends Error {
1128
1180
  }
1129
1181
  };
1130
1182
 
1131
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-helpers.js
1183
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-helpers.js
1132
1184
  function compareDirEntryNames(a, b) {
1185
+ const ua = a.toUpperCase();
1186
+ const ub = b.toUpperCase();
1187
+ if (ua.length !== a.length || ub.length !== b.length) {
1188
+ throw new Error("CFB stream names with expanding uppercase mappings are unsupported");
1189
+ }
1133
1190
  if (a.length !== b.length) {
1134
1191
  return a.length - b.length;
1135
1192
  }
1136
- const ua = a.toUpperCase();
1137
- const ub = b.toUpperCase();
1138
1193
  for (let i = 0; i < ua.length; i++) {
1139
1194
  const diff = ua.charCodeAt(i) - ub.charCodeAt(i);
1140
1195
  if (diff !== 0) {
@@ -1151,6 +1206,24 @@ function encodeName(name) {
1151
1206
  }
1152
1207
  return bytes;
1153
1208
  }
1209
+ function buildDirectoryTree(entryCount) {
1210
+ const links = /* @__PURE__ */ new Map();
1211
+ let redLevel = 0;
1212
+ for (let remaining = entryCount - 2; remaining >= 0; remaining = Math.floor(remaining / 2) - 1)
1213
+ redLevel++;
1214
+ const build = (low, high, level) => {
1215
+ if (low > high)
1216
+ return 4294967295;
1217
+ const id = Math.floor((low + high) / 2);
1218
+ links.set(id, {
1219
+ left: build(low, id - 1, level + 1),
1220
+ right: build(id + 1, high, level + 1),
1221
+ color: level === redLevel ? 0 : 1
1222
+ });
1223
+ return id;
1224
+ };
1225
+ return { root: build(1, entryCount - 1, 0), links };
1226
+ }
1154
1227
  function writeFatChain(fat, chain) {
1155
1228
  for (let i = 0; i < chain.sectors.length; i++) {
1156
1229
  fat[chain.sectors[i]] = i < chain.sectors.length - 1 ? chain.sectors[i + 1] : ENDOFCHAIN;
@@ -1174,7 +1247,7 @@ function writeInt32Sectors(outBytes, int32Data, firstSector, numSectors, sectorS
1174
1247
  }
1175
1248
  }
1176
1249
 
1177
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-serialize.js
1250
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-serialize.js
1178
1251
  function writeHeader(outView, outBytes, params) {
1179
1252
  outBytes.set(OLE_MAGIC, 0);
1180
1253
  outView.setUint16(24, 62, true);
@@ -1232,6 +1305,7 @@ function writeDifatSectors(outView, params) {
1232
1305
  function serializeDirectoryEntries(dirEntries, numDirSectors, sectorSize) {
1233
1306
  const dirData = new Uint8Array(numDirSectors * sectorSize);
1234
1307
  const dirView = new DataView(dirData.buffer);
1308
+ const tree = buildDirectoryTree(dirEntries.length);
1235
1309
  for (let i = 0; i < dirEntries.length; i++) {
1236
1310
  const entry = dirEntries[i];
1237
1311
  const entryOffset = i * DIR_ENTRY_SIZE;
@@ -1239,17 +1313,17 @@ function serializeDirectoryEntries(dirEntries, numDirSectors, sectorSize) {
1239
1313
  dirData.set(nameBytes.subarray(0, Math.min(nameBytes.length, 64)), entryOffset);
1240
1314
  dirView.setUint16(entryOffset + 64, Math.min((entry.name.length + 1) * 2, 64), true);
1241
1315
  dirData[entryOffset + 66] = entry.type;
1242
- dirData[entryOffset + 67] = 1;
1316
+ dirData[entryOffset + 67] = i === 0 ? 1 : tree.links.get(i).color;
1243
1317
  if (entry.clsid) {
1244
1318
  dirData.set(entry.clsid.subarray(0, 16), entryOffset + 80);
1245
1319
  }
1246
1320
  if (i === 0) {
1247
1321
  dirView.setUint32(entryOffset + 68, 4294967295, true);
1248
1322
  dirView.setUint32(entryOffset + 72, 4294967295, true);
1249
- dirView.setUint32(entryOffset + 76, dirEntries.length > 1 ? 1 : 4294967295, true);
1323
+ dirView.setUint32(entryOffset + 76, tree.root, true);
1250
1324
  } else {
1251
- dirView.setUint32(entryOffset + 68, 4294967295, true);
1252
- dirView.setUint32(entryOffset + 72, i + 1 < dirEntries.length ? i + 1 : 4294967295, true);
1325
+ dirView.setUint32(entryOffset + 68, tree.links.get(i).left, true);
1326
+ dirView.setUint32(entryOffset + 72, tree.links.get(i).right, true);
1253
1327
  dirView.setUint32(entryOffset + 76, 4294967295, true);
1254
1328
  }
1255
1329
  dirView.setUint32(entryOffset + 116, entry.startSector, true);
@@ -1266,10 +1340,23 @@ function writeStreamSectors(outBytes, streamData, chain, sectorSize) {
1266
1340
  }
1267
1341
  }
1268
1342
 
1269
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write.js
1343
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write.js
1270
1344
  function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1271
1345
  const sectorSize = 512;
1272
1346
  const miniSectorSize = 64;
1347
+ const names = [...streams2.keys()].sort(compareDirEntryNames);
1348
+ for (let i = 0; i < names.length; i++) {
1349
+ const name = names[i];
1350
+ if (name.toUpperCase().length !== name.length) {
1351
+ throw new Error("CFB stream names with expanding uppercase mappings are unsupported");
1352
+ }
1353
+ if (!name.length || name.length > 31 || /[\x00\\/:!]/.test(name)) {
1354
+ throw new Error("CFB stream names must be 1-31 UTF-16 code units without NUL, \\, /, :, or !");
1355
+ }
1356
+ if (i > 0 && compareDirEntryNames(names[i - 1], name) === 0) {
1357
+ throw new Error("Duplicate CFB stream names");
1358
+ }
1359
+ }
1273
1360
  const regularStreams = [];
1274
1361
  const miniStreams = [];
1275
1362
  for (const [name, data] of streams2) {
@@ -1287,7 +1374,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1287
1374
  for (let i = 0; i < numSectors; i++) {
1288
1375
  sectors.push(nextSector++);
1289
1376
  }
1290
- fatChains.set(stream.name, { start: sectors[0] ?? 0, sectors });
1377
+ fatChains.set(stream.name, { start: sectors[0] ?? ENDOFCHAIN, sectors });
1291
1378
  }
1292
1379
  let miniStreamContainer = new Uint8Array(0);
1293
1380
  const miniFatChains = /* @__PURE__ */ new Map();
@@ -1310,7 +1397,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1310
1397
  miniOffset += miniSectorSize;
1311
1398
  }
1312
1399
  miniFatChains.set(stream.name, {
1313
- start: miniSectors[0] ?? 0,
1400
+ start: miniSectors[0] ?? ENDOFCHAIN,
1314
1401
  sectors: miniSectors
1315
1402
  });
1316
1403
  }
@@ -1358,7 +1445,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1358
1445
  nextSector += numDirSectors;
1359
1446
  let firstMiniFATSector = ENDOFCHAIN;
1360
1447
  let numMiniFATSectors = 0;
1361
- if (miniStreams.length > 0) {
1448
+ if (nextMiniSector > 0) {
1362
1449
  numMiniFATSectors = Math.ceil(nextMiniSector * 4 / sectorSize);
1363
1450
  firstMiniFATSector = nextSector;
1364
1451
  nextSector += numMiniFATSectors;
@@ -1,4 +1,4 @@
1
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-types.js
1
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-types.js
2
2
  var OLE_MAGIC = new Uint8Array([208, 207, 17, 224, 161, 177, 26, 225]);
3
3
  var ENDOFCHAIN = 4294967294;
4
4
  var FREESECT = 4294967295;
@@ -16,10 +16,13 @@ var Ole2ParseError = class extends Error {
16
16
  }
17
17
  };
18
18
 
19
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-read.js
19
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-read.js
20
20
  function parseOle2(buffer) {
21
21
  const data = new Uint8Array(buffer);
22
22
  const view = new DataView(buffer);
23
+ if (data.length < 512) {
24
+ throw new Ole2ParseError("Truncated OLE2 compound file header");
25
+ }
23
26
  for (let i = 0; i < OLE_MAGIC.length; i++) {
24
27
  if (data[i] !== OLE_MAGIC[i]) {
25
28
  throw new Ole2ParseError("Not a valid OLE2 compound file");
@@ -33,8 +36,15 @@ function parseOle2(buffer) {
33
36
  }
34
37
  const sectorSizePower = view.getUint16(30, true);
35
38
  const miniSectorSizePower = view.getUint16(32, true);
39
+ if (majorVersion !== 3 && majorVersion !== 4 || sectorSizePower !== (majorVersion === 3 ? 9 : 12) || miniSectorSizePower !== 6) {
40
+ throw new Ole2ParseError("Unsupported OLE2 version or sector size");
41
+ }
36
42
  const sectorSize = 1 << sectorSizePower;
37
43
  const miniSectorSize = 1 << miniSectorSizePower;
44
+ if (data.length < sectorSize || data.length % sectorSize !== 0) {
45
+ throw new Ole2ParseError("Truncated OLE2 sector");
46
+ }
47
+ const sectorCount = data.length / sectorSize - 1;
38
48
  const totalFATSectors = view.getUint32(44, true);
39
49
  const firstDirectorySector = view.getUint32(48, true);
40
50
  const miniStreamCutoff = view.getUint32(56, true);
@@ -42,6 +52,9 @@ function parseOle2(buffer) {
42
52
  const totalMiniFATSectors = view.getUint32(64, true);
43
53
  const firstDIFATSector = view.getUint32(68, true);
44
54
  const totalDIFATSectors = view.getUint32(72, true);
55
+ if (totalFATSectors > sectorCount || totalMiniFATSectors > sectorCount || totalDIFATSectors > sectorCount) {
56
+ throw new Ole2ParseError("Allocation table count exceeds file size");
57
+ }
45
58
  function sectorOffset(sector) {
46
59
  return (sector + 1) * sectorSize;
47
60
  }
@@ -60,7 +73,11 @@ function parseOle2(buffer) {
60
73
  }
61
74
  }
62
75
  let difatSector = firstDIFATSector;
76
+ const difatSeen = /* @__PURE__ */ new Set();
63
77
  for (let d = 0; d < totalDIFATSectors && difatSector <= MAXREGSECT; d++) {
78
+ if (difatSeen.has(difatSector))
79
+ throw new Ole2ParseError("Circular DIFAT chain");
80
+ difatSeen.add(difatSector);
64
81
  const difatData = readSector(difatSector);
65
82
  const difatView = new DataView(difatData.buffer, difatData.byteOffset, difatData.byteLength);
66
83
  const entriesPerSector = (sectorSize - 4) / 4;
@@ -72,11 +89,14 @@ function parseOle2(buffer) {
72
89
  }
73
90
  difatSector = difatView.getUint32(sectorSize - 4, true);
74
91
  }
92
+ if (difatSeen.size !== totalDIFATSectors || fatSectors.length !== totalFATSectors || new Set(fatSectors).size !== fatSectors.length || fatSectors.some((id) => difatSeen.has(id))) {
93
+ throw new Ole2ParseError("Invalid DIFAT allocation table");
94
+ }
75
95
  const fatEntries = [];
76
96
  for (const fatSector of fatSectors) {
77
97
  const fatData = readSector(fatSector);
78
98
  const fatView = new DataView(fatData.buffer, fatData.byteOffset, fatData.byteLength);
79
- for (let i = 0; i < sectorSize / 4; i++) {
99
+ for (let i = 0; i < sectorSize / 4 && fatEntries.length < sectorCount; i++) {
80
100
  fatEntries.push(fatView.getUint32(i * 4, true));
81
101
  }
82
102
  }
@@ -90,8 +110,12 @@ function parseOle2(buffer) {
90
110
  }
91
111
  visited.add(current);
92
112
  sectors.push(readSector(current));
93
- current = fatEntries[current] ?? ENDOFCHAIN;
113
+ if (fatEntries[current] === void 0)
114
+ throw new Ole2ParseError("Missing FAT chain entry");
115
+ current = fatEntries[current];
94
116
  }
117
+ if (current !== ENDOFCHAIN)
118
+ throw new Ole2ParseError("Invalid FAT chain terminator");
95
119
  const totalLength = sectors.length * sectorSize;
96
120
  const result = new Uint8Array(totalLength);
97
121
  let offset = 0;
@@ -102,16 +126,14 @@ function parseOle2(buffer) {
102
126
  return result;
103
127
  }
104
128
  function readStream(startSector, size) {
129
+ if (size === 0)
130
+ return new Uint8Array(0);
131
+ if (size > sectorCount * sectorSize)
132
+ throw new Ole2ParseError("Stream size exceeds file size");
105
133
  const raw = readSectorChain(startSector);
106
- return raw.subarray(0, Math.min(size, raw.length));
107
- }
108
- const miniFatEntries = [];
109
- if (firstMiniFATSector <= MAXREGSECT && totalMiniFATSectors > 0) {
110
- const miniFatRaw = readSectorChain(firstMiniFATSector);
111
- const miniFatView = new DataView(miniFatRaw.buffer, miniFatRaw.byteOffset, miniFatRaw.byteLength);
112
- for (let i = 0; i < miniFatRaw.length / 4; i++) {
113
- miniFatEntries.push(miniFatView.getUint32(i * 4, true));
114
- }
134
+ if (raw.length < size)
135
+ throw new Ole2ParseError("Truncated stream sector chain");
136
+ return raw.subarray(0, size);
115
137
  }
116
138
  const dirRaw = readSectorChain(firstDirectorySector);
117
139
  const numEntries = Math.floor(dirRaw.length / DIR_ENTRY_SIZE);
@@ -124,6 +146,9 @@ function parseOle2(buffer) {
124
146
  if (objectType === ENTRY_TYPE_EMPTY) {
125
147
  continue;
126
148
  }
149
+ if (nameLen < 2 || nameLen > 64 || nameLen % 2 !== 0 || entryView.getUint16(nameLen - 2, true) !== 0) {
150
+ throw new Ole2ParseError("Invalid directory entry name");
151
+ }
127
152
  const nameBytes = Math.max(0, nameLen - 2);
128
153
  let name = "";
129
154
  for (let j = 0; j < nameBytes; j += 2) {
@@ -137,8 +162,11 @@ function parseOle2(buffer) {
137
162
  const sizeLow = entryView.getUint32(120, true);
138
163
  let size = sizeLow;
139
164
  if (majorVersion === 4) {
140
- entryView.getUint32(124, true);
141
- size = sizeLow;
165
+ const sizeHigh = entryView.getUint32(124, true);
166
+ size = sizeLow + sizeHigh * 4294967296;
167
+ if (!Number.isSafeInteger(size) || size > sectorCount * sectorSize) {
168
+ throw new Ole2ParseError("Stream size exceeds supported file bounds");
169
+ }
142
170
  }
143
171
  entries.push({
144
172
  name,
@@ -152,11 +180,28 @@ function parseOle2(buffer) {
152
180
  });
153
181
  }
154
182
  const rootEntry = entries.find((e) => e.type === ENTRY_TYPE_ROOT);
183
+ if (!rootEntry || entries.filter((e) => e.type === ENTRY_TYPE_ROOT).length !== 1) {
184
+ throw new Ole2ParseError("Invalid root directory entry");
185
+ }
155
186
  let miniStreamData;
156
187
  if (rootEntry && rootEntry.startSector <= MAXREGSECT) {
157
- miniStreamData = readSectorChain(rootEntry.startSector);
188
+ miniStreamData = readStream(rootEntry.startSector, rootEntry.size);
189
+ }
190
+ const miniFatEntries = [];
191
+ if (firstMiniFATSector <= MAXREGSECT && totalMiniFATSectors > 0) {
192
+ const miniFatRaw = readSectorChain(firstMiniFATSector);
193
+ if (miniFatRaw.length !== totalMiniFATSectors * sectorSize) {
194
+ throw new Ole2ParseError("Unexpected mini FAT chain length");
195
+ }
196
+ const miniFatView = new DataView(miniFatRaw.buffer, miniFatRaw.byteOffset, miniFatRaw.byteLength);
197
+ const miniCapacity = Math.floor((miniStreamData?.length ?? 0) / miniSectorSize);
198
+ for (let i = 0; i < miniFatRaw.length / 4 && i < miniCapacity; i++) {
199
+ miniFatEntries.push(miniFatView.getUint32(i * 4, true));
200
+ }
158
201
  }
159
202
  function readMiniStream(startSector, size) {
203
+ if (size === 0)
204
+ return new Uint8Array(0);
160
205
  if (!miniStreamData) {
161
206
  throw new Ole2ParseError("Mini stream container not found");
162
207
  }
@@ -169,17 +214,24 @@ function parseOle2(buffer) {
169
214
  }
170
215
  visited.add(current);
171
216
  const offset2 = current * miniSectorSize;
217
+ if (offset2 + miniSectorSize > miniStreamData.length || miniFatEntries[current] === void 0) {
218
+ throw new Ole2ParseError("Mini stream sector exceeds allocation bounds");
219
+ }
172
220
  sectors.push(miniStreamData.subarray(offset2, offset2 + miniSectorSize));
173
- current = miniFatEntries[current] ?? ENDOFCHAIN;
221
+ current = miniFatEntries[current];
174
222
  }
223
+ if (current !== ENDOFCHAIN)
224
+ throw new Ole2ParseError("Invalid mini FAT chain terminator");
175
225
  const totalLength = sectors.length * miniSectorSize;
226
+ if (totalLength < size)
227
+ throw new Ole2ParseError("Truncated mini stream sector chain");
176
228
  const result = new Uint8Array(totalLength);
177
229
  let offset = 0;
178
230
  for (const sector of sectors) {
179
231
  result.set(sector, offset);
180
232
  offset += miniSectorSize;
181
233
  }
182
- return result.subarray(0, Math.min(size, result.length));
234
+ return result.subarray(0, size);
183
235
  }
184
236
  function getStream(name) {
185
237
  const entry = entries.find((e) => (e.type === ENTRY_TYPE_STREAM || e.type === ENTRY_TYPE_ROOT) && e.name === name);
@@ -194,7 +246,7 @@ function parseOle2(buffer) {
194
246
  return { entries, getStream };
195
247
  }
196
248
 
197
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/utils/ooxml-crypto-primitives.js
249
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/utils/ooxml-crypto-primitives.js
198
250
  function getSubtle() {
199
251
  if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
200
252
  return globalThis.crypto.subtle;
@@ -1126,13 +1178,16 @@ var EncryptedFileError = class extends Error {
1126
1178
  }
1127
1179
  };
1128
1180
 
1129
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-helpers.js
1181
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-helpers.js
1130
1182
  function compareDirEntryNames(a, b) {
1183
+ const ua = a.toUpperCase();
1184
+ const ub = b.toUpperCase();
1185
+ if (ua.length !== a.length || ub.length !== b.length) {
1186
+ throw new Error("CFB stream names with expanding uppercase mappings are unsupported");
1187
+ }
1131
1188
  if (a.length !== b.length) {
1132
1189
  return a.length - b.length;
1133
1190
  }
1134
- const ua = a.toUpperCase();
1135
- const ub = b.toUpperCase();
1136
1191
  for (let i = 0; i < ua.length; i++) {
1137
1192
  const diff = ua.charCodeAt(i) - ub.charCodeAt(i);
1138
1193
  if (diff !== 0) {
@@ -1149,6 +1204,24 @@ function encodeName(name) {
1149
1204
  }
1150
1205
  return bytes;
1151
1206
  }
1207
+ function buildDirectoryTree(entryCount) {
1208
+ const links = /* @__PURE__ */ new Map();
1209
+ let redLevel = 0;
1210
+ for (let remaining = entryCount - 2; remaining >= 0; remaining = Math.floor(remaining / 2) - 1)
1211
+ redLevel++;
1212
+ const build = (low, high, level) => {
1213
+ if (low > high)
1214
+ return 4294967295;
1215
+ const id = Math.floor((low + high) / 2);
1216
+ links.set(id, {
1217
+ left: build(low, id - 1, level + 1),
1218
+ right: build(id + 1, high, level + 1),
1219
+ color: level === redLevel ? 0 : 1
1220
+ });
1221
+ return id;
1222
+ };
1223
+ return { root: build(1, entryCount - 1, 0), links };
1224
+ }
1152
1225
  function writeFatChain(fat, chain) {
1153
1226
  for (let i = 0; i < chain.sectors.length; i++) {
1154
1227
  fat[chain.sectors[i]] = i < chain.sectors.length - 1 ? chain.sectors[i + 1] : ENDOFCHAIN;
@@ -1172,7 +1245,7 @@ function writeInt32Sectors(outBytes, int32Data, firstSector, numSectors, sectorS
1172
1245
  }
1173
1246
  }
1174
1247
 
1175
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-serialize.js
1248
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write-serialize.js
1176
1249
  function writeHeader(outView, outBytes, params) {
1177
1250
  outBytes.set(OLE_MAGIC, 0);
1178
1251
  outView.setUint16(24, 62, true);
@@ -1230,6 +1303,7 @@ function writeDifatSectors(outView, params) {
1230
1303
  function serializeDirectoryEntries(dirEntries, numDirSectors, sectorSize) {
1231
1304
  const dirData = new Uint8Array(numDirSectors * sectorSize);
1232
1305
  const dirView = new DataView(dirData.buffer);
1306
+ const tree = buildDirectoryTree(dirEntries.length);
1233
1307
  for (let i = 0; i < dirEntries.length; i++) {
1234
1308
  const entry = dirEntries[i];
1235
1309
  const entryOffset = i * DIR_ENTRY_SIZE;
@@ -1237,17 +1311,17 @@ function serializeDirectoryEntries(dirEntries, numDirSectors, sectorSize) {
1237
1311
  dirData.set(nameBytes.subarray(0, Math.min(nameBytes.length, 64)), entryOffset);
1238
1312
  dirView.setUint16(entryOffset + 64, Math.min((entry.name.length + 1) * 2, 64), true);
1239
1313
  dirData[entryOffset + 66] = entry.type;
1240
- dirData[entryOffset + 67] = 1;
1314
+ dirData[entryOffset + 67] = i === 0 ? 1 : tree.links.get(i).color;
1241
1315
  if (entry.clsid) {
1242
1316
  dirData.set(entry.clsid.subarray(0, 16), entryOffset + 80);
1243
1317
  }
1244
1318
  if (i === 0) {
1245
1319
  dirView.setUint32(entryOffset + 68, 4294967295, true);
1246
1320
  dirView.setUint32(entryOffset + 72, 4294967295, true);
1247
- dirView.setUint32(entryOffset + 76, dirEntries.length > 1 ? 1 : 4294967295, true);
1321
+ dirView.setUint32(entryOffset + 76, tree.root, true);
1248
1322
  } else {
1249
- dirView.setUint32(entryOffset + 68, 4294967295, true);
1250
- dirView.setUint32(entryOffset + 72, i + 1 < dirEntries.length ? i + 1 : 4294967295, true);
1323
+ dirView.setUint32(entryOffset + 68, tree.links.get(i).left, true);
1324
+ dirView.setUint32(entryOffset + 72, tree.links.get(i).right, true);
1251
1325
  dirView.setUint32(entryOffset + 76, 4294967295, true);
1252
1326
  }
1253
1327
  dirView.setUint32(entryOffset + 116, entry.startSector, true);
@@ -1264,10 +1338,23 @@ function writeStreamSectors(outBytes, streamData, chain, sectorSize) {
1264
1338
  }
1265
1339
  }
1266
1340
 
1267
- // node_modules/.bun/@christophervr+ole2@0.4.0/node_modules/@christophervr/ole2/dist/ole2-parser-write.js
1341
+ // node_modules/.bun/@christophervr+ole2@0.8.0/node_modules/@christophervr/ole2/dist/ole2-parser-write.js
1268
1342
  function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1269
1343
  const sectorSize = 512;
1270
1344
  const miniSectorSize = 64;
1345
+ const names = [...streams2.keys()].sort(compareDirEntryNames);
1346
+ for (let i = 0; i < names.length; i++) {
1347
+ const name = names[i];
1348
+ if (name.toUpperCase().length !== name.length) {
1349
+ throw new Error("CFB stream names with expanding uppercase mappings are unsupported");
1350
+ }
1351
+ if (!name.length || name.length > 31 || /[\x00\\/:!]/.test(name)) {
1352
+ throw new Error("CFB stream names must be 1-31 UTF-16 code units without NUL, \\, /, :, or !");
1353
+ }
1354
+ if (i > 0 && compareDirEntryNames(names[i - 1], name) === 0) {
1355
+ throw new Error("Duplicate CFB stream names");
1356
+ }
1357
+ }
1271
1358
  const regularStreams = [];
1272
1359
  const miniStreams = [];
1273
1360
  for (const [name, data] of streams2) {
@@ -1285,7 +1372,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1285
1372
  for (let i = 0; i < numSectors; i++) {
1286
1373
  sectors.push(nextSector++);
1287
1374
  }
1288
- fatChains.set(stream.name, { start: sectors[0] ?? 0, sectors });
1375
+ fatChains.set(stream.name, { start: sectors[0] ?? ENDOFCHAIN, sectors });
1289
1376
  }
1290
1377
  let miniStreamContainer = new Uint8Array(0);
1291
1378
  const miniFatChains = /* @__PURE__ */ new Map();
@@ -1308,7 +1395,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1308
1395
  miniOffset += miniSectorSize;
1309
1396
  }
1310
1397
  miniFatChains.set(stream.name, {
1311
- start: miniSectors[0] ?? 0,
1398
+ start: miniSectors[0] ?? ENDOFCHAIN,
1312
1399
  sectors: miniSectors
1313
1400
  });
1314
1401
  }
@@ -1356,7 +1443,7 @@ function buildOle2(streams2, rootClsid, miniStreamCutoff = 4096) {
1356
1443
  nextSector += numDirSectors;
1357
1444
  let firstMiniFATSector = ENDOFCHAIN;
1358
1445
  let numMiniFATSectors = 0;
1359
- if (miniStreams.length > 0) {
1446
+ if (nextMiniSector > 0) {
1360
1447
  numMiniFATSectors = Math.ceil(nextMiniSector * 4 / sectorSize);
1361
1448
  firstMiniFATSector = nextSector;
1362
1449
  nextSector += numMiniFATSectors;