@shbernal/ts-xlsx 3.2.2-next.24.g38fae0e → 3.2.2-next.26.gf63f249

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.
@@ -2,7 +2,7 @@ import { encodeAddress, MAX_COLUMN, MAX_ROW } from '../../core/address.js';
2
2
  import { isCustomFilterOperator, } from '../../core/autofilter.js';
3
3
  import { HEADER_FOOTER_ELEMENTS, MARGIN_SIDES, PAGE_SETUP_FACETS, PRINT_OPTION_FLAGS, } from '../../core/page-setup.js';
4
4
  import { isLegacyPasswordHash, SHEET_PROTECTION_FLAGS, } from '../../core/protection.js';
5
- import { assertWritableNumber, AuthoringError } from '../../errors.js';
5
+ import { AuthoringError } from '../../errors.js';
6
6
  import { numFinite, numInteger } from '../../xml/xml-attrs.js';
7
7
  import { boolStrict, boolTristate } from '../../xml/xml-scan.js';
8
8
  import { boolAttr, checkedToken, escapeAttr, escapeSpreadsheetText, numAttr, numberText, } from '../../xml/xml.js';
@@ -29,7 +29,6 @@ export function sheetViewsXml(view, active) {
29
29
  function frozenSplit(name, value, lines) {
30
30
  if (value === undefined)
31
31
  return 0;
32
- assertWritableNumber(value);
33
32
  if (!Number.isInteger(value) || value < 0 || value >= lines) {
34
33
  throw new AuthoringError(`cannot write ${name}="${value}": a frozen split is a whole count from 0 to ${lines - 1}`);
35
34
  }
@@ -1,7 +1,6 @@
1
1
  import { type Alignment, type Border, type Fill, type Protection } from '../../core/style.ts';
2
2
  import type { DifferentialStyle } from '../../core/workbook-styles.ts';
3
3
  export declare const CUSTOM_NUMFMT_BASE = 164;
4
- export declare const DEFAULT_FONT_BODY = "<sz val=\"11\"/><color theme=\"1\"/><name val=\"Calibri\"/><family val=\"2\"/><scheme val=\"minor\"/>";
5
4
  export declare const DEFAULT_BORDER = "<border><left/><right/><top/><bottom/><diagonal/></border>";
6
5
  export interface CellFormat {
7
6
  readonly fillId: number;
@@ -3,7 +3,6 @@ import { checkedToken, escapeAttr, escapeFormatCode, nonDefaultNumAttr, numberTe
3
3
  import { colorAttrs } from './color-xml.js';
4
4
  import { fontXml } from './font-xml.js';
5
5
  export const CUSTOM_NUMFMT_BASE = 164;
6
- export const DEFAULT_FONT_BODY = '<sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/><scheme val="minor"/>';
7
6
  export const DEFAULT_BORDER = '<border><left/><right/><top/><bottom/><diagonal/></border>';
8
7
  export const DEFAULT_FORMAT = {
9
8
  fillId: 0,
@@ -8,9 +8,9 @@ export interface StyleRegistryOptions {
8
8
  /**
9
9
  * The font every cell naming none of its own renders in, emitted as id 0. Take it from
10
10
  * {@link Workbook.defaultFont}, which resolves the authored/declared/theme chain and guarantees a
11
- * complete entry. Omitted, the registry falls back to the Office default.
11
+ * complete entry.
12
12
  */
13
- readonly defaultFont?: Font;
13
+ readonly defaultFont: Font;
14
14
  /**
15
15
  * The default font the *source package* declared, when a workbook was read
16
16
  * ({@link Workbook.declaredDefaultFont}). It also interns to id 0, which is what lets an authored
@@ -34,7 +34,7 @@ export interface StyleRegistryOptions {
34
34
  */
35
35
  export declare class StyleRegistry {
36
36
  #private;
37
- constructor(options?: StyleRegistryOptions);
37
+ constructor(options: StyleRegistryOptions);
38
38
  /**
39
39
  * The `<cellXfs>` index for a composed cell/row/column style. A style with no facet needs
40
40
  * no entry and resolves to the default xf 0, so its owner emits no `s` attribute at all.
@@ -3,7 +3,7 @@ import { TABLE_STYLE_ELEMENT_TYPES } from '../../core/table-style.js';
3
3
  import { escapeAttr, escapeFormatCode, nonDefaultNumAttr, XML_DECLARATION } from '../../xml/xml.js';
4
4
  import { fontXml } from './font-xml.js';
5
5
  import { MARKUP_COMPATIBILITY_NS, SPREADSHEETML_NS } from './namespaces.js';
6
- import { alignmentAttrs, borderXml, cellStyleTag, CUSTOM_NUMFMT_BASE, DEFAULT_BORDER, DEFAULT_FONT_BODY, DEFAULT_FORMAT, dxfXml, formatSignature, isDefaultFormat, patternFillXml, protectionAttrs, xfXml, } from './style-elements.js';
6
+ import { alignmentAttrs, borderXml, cellStyleTag, CUSTOM_NUMFMT_BASE, DEFAULT_BORDER, DEFAULT_FORMAT, dxfXml, formatSignature, isDefaultFormat, patternFillXml, protectionAttrs, xfXml, } from './style-elements.js';
7
7
  const RESERVED_FILL_COUNT = 2;
8
8
  const RESERVED_FONT_COUNT = 1;
9
9
  const RESERVED_BORDER_COUNT = 1;
@@ -40,9 +40,8 @@ class InternTable {
40
40
  export class StyleRegistry {
41
41
  #defaultFontBody;
42
42
  #font0Bodies;
43
- constructor(options = {}) {
44
- this.#defaultFontBody =
45
- options.defaultFont === undefined ? DEFAULT_FONT_BODY : fontXml(options.defaultFont);
43
+ constructor(options) {
44
+ this.#defaultFontBody = fontXml(options.defaultFont);
46
45
  this.#font0Bodies = new Set(options.declaredDefaultFont === undefined
47
46
  ? [this.#defaultFontBody]
48
47
  : [this.#defaultFontBody, fontXml(options.declaredDefaultFont)]);
@@ -42,7 +42,6 @@ export declare const MINI_SECTOR_SHIFT = 6;
42
42
  export declare const FREESECT = 4294967295;
43
43
  export declare const ENDOFCHAIN = 4294967294;
44
44
  export declare const FATSECT = 4294967293;
45
- export declare const DIFSECT = 4294967292;
46
45
  /**
47
46
  * Sector values 0xFFFFFFFA..0xFFFFFFFF are the reserved markers above, not data-sector indices, so
48
47
  * any value at or above this ceiling is chain-terminal. A reader walking a hostile chain tests the
@@ -77,3 +76,9 @@ export declare const MAX_NAME_CHARS = 31;
77
76
  * name differently would read as missing a stream it has.
78
77
  */
79
78
  export declare function sameEntryName(a: string, b: string): boolean;
79
+ /**
80
+ * A key equal for exactly the names {@link sameEntryName} treats as one entry, so a set of them can
81
+ * catch a collision without comparing every pair. The length is part of the key because uppercasing
82
+ * can change it (`ß` becomes `SS`), and 2.6.4 compares the original lengths first.
83
+ */
84
+ export declare function entryNameKey(name: string): string;
@@ -32,7 +32,6 @@ export const MINI_SECTOR_SHIFT = 6;
32
32
  export const FREESECT = 0xffffffff;
33
33
  export const ENDOFCHAIN = 0xfffffffe;
34
34
  export const FATSECT = 0xfffffffd;
35
- export const DIFSECT = 0xfffffffc;
36
35
  export const MAX_REGULAR_SECTOR = 0xfffffffa;
37
36
  export const NOSTREAM = 0xffffffff;
38
37
  export const TYPE_EMPTY = 0;
@@ -43,5 +42,8 @@ export const MINI_STREAM_CUTOFF = 4096;
43
42
  export const DIR_ENTRY_SIZE = 128;
44
43
  export const MAX_NAME_CHARS = 31;
45
44
  export function sameEntryName(a, b) {
46
- return a.length === b.length && a.toUpperCase() === b.toUpperCase();
45
+ return entryNameKey(a) === entryNameKey(b);
46
+ }
47
+ export function entryNameKey(name) {
48
+ return `${name.length}:${name.toUpperCase()}`;
47
49
  }
@@ -1,5 +1,5 @@
1
1
  import { quoted } from '../errors.js';
2
- import { CFB_SIGNATURE, DIFSECT, DIR_ENTRY_FIELD, DIR_ENTRY_SIZE, ENDOFCHAIN, FATSECT, FREESECT, HEADER_DIFAT_SLOTS, HEADER_FIELD, MAX_NAME_CHARS, MINI_SECTOR_SHIFT, MINI_STREAM_CUTOFF, NOSTREAM, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
2
+ import { CFB_SIGNATURE, DIR_ENTRY_FIELD, DIR_ENTRY_SIZE, ENDOFCHAIN, entryNameKey, FATSECT, FREESECT, HEADER_DIFAT_SLOTS, HEADER_FIELD, MAX_NAME_CHARS, MINI_SECTOR_SHIFT, MINI_STREAM_CUTOFF, NOSTREAM, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
3
3
  import { VbaAuthorError } from './errors.js';
4
4
  export function isStream(node) {
5
5
  return 'data' in node;
@@ -75,17 +75,11 @@ export function writeCompoundFile(root) {
75
75
  miniStreamSectors +
76
76
  bigStreams.reduce((total, big) => total + big.sectors, 0);
77
77
  let fatSectors = 0;
78
- let difatSectors = 0;
79
78
  for (;;) {
80
- const total = baseSectors + fatSectors + difatSectors;
81
- const needFat = Math.ceil(total / FAT_ENTRIES_PER_SECTOR);
82
- const needDifat = needFat > HEADER_DIFAT_SLOTS
83
- ? Math.ceil((needFat - HEADER_DIFAT_SLOTS) / (FAT_ENTRIES_PER_SECTOR - 1))
84
- : 0;
85
- if (needFat === fatSectors && needDifat === difatSectors)
79
+ const needFat = Math.ceil((baseSectors + fatSectors) / FAT_ENTRIES_PER_SECTOR);
80
+ if (needFat === fatSectors)
86
81
  break;
87
82
  fatSectors = needFat;
88
- difatSectors = needDifat;
89
83
  }
90
84
  if (fatSectors > HEADER_DIFAT_SLOTS) {
91
85
  throw new VbaAuthorError(`project needs ${fatSectors} FAT sectors, exceeding the ${HEADER_DIFAT_SLOTS}-sector single-header bound`);
@@ -103,8 +97,6 @@ export function writeCompoundFile(root) {
103
97
  }
104
98
  const fatStart = cursor;
105
99
  cursor += fatSectors;
106
- const difatStart = difatSectors > 0 ? cursor : ENDOFCHAIN;
107
- cursor += difatSectors;
108
100
  const totalSectors = cursor;
109
101
  rootEntry.startSector = miniStreamStart;
110
102
  rootEntry.size = miniLength;
@@ -122,8 +114,6 @@ export function writeCompoundFile(root) {
122
114
  chainRegion(big.entry.startSector, big.sectors);
123
115
  for (let k = 0; k < fatSectors; k++)
124
116
  fat[fatStart + k] = FATSECT;
125
- for (let k = 0; k < difatSectors; k++)
126
- fat[difatStart + k] = DIFSECT;
127
117
  const buf = new Uint8Array((totalSectors + 1) * SECTOR);
128
118
  const dv = new DataView(buf.buffer);
129
119
  const at = (sector) => (sector + 1) * SECTOR;
@@ -132,8 +122,6 @@ export function writeCompoundFile(root) {
132
122
  dirStart,
133
123
  miniFatStart,
134
124
  miniFatSectors,
135
- difatStart,
136
- difatSectors,
137
125
  fatStart,
138
126
  });
139
127
  for (const e of entries)
@@ -152,14 +140,6 @@ export function writeCompoundFile(root) {
152
140
  buf.set(big.data, at(big.entry.startSector));
153
141
  for (const [i, sector] of fat.entries())
154
142
  dv.setUint32(at(fatStart) + i * 4, sector, true);
155
- for (let d = 0; d < difatSectors; d++) {
156
- const base = at(difatStart + d);
157
- for (let i = 0; i < FAT_ENTRIES_PER_SECTOR - 1; i++) {
158
- const fatIdx = HEADER_DIFAT_SLOTS + d * (FAT_ENTRIES_PER_SECTOR - 1) + i;
159
- dv.setUint32(base + i * 4, fatIdx < fatSectors ? fatStart + fatIdx : FREESECT, true);
160
- }
161
- dv.setUint32(base + (FAT_ENTRIES_PER_SECTOR - 1) * 4, d < difatSectors - 1 ? difatStart + d + 1 : ENDOFCHAIN, true);
162
- }
163
143
  return buf;
164
144
  }
165
145
  function validateSiblingNames(storageName, siblings) {
@@ -170,10 +150,11 @@ function validateSiblingNames(storageName, siblings) {
170
150
  if (node.name.length > MAX_NAME_CHARS) {
171
151
  throw new VbaAuthorError(`entry name ${quoted(node.name)} exceeds the ${MAX_NAME_CHARS}-character CFB limit`);
172
152
  }
173
- if (seen.has(node.name)) {
153
+ const key = entryNameKey(node.name);
154
+ if (seen.has(key)) {
174
155
  throw new VbaAuthorError(`duplicate entry name ${quoted(node.name)} under storage ${quoted(storageName)}`);
175
156
  }
176
- seen.add(node.name);
157
+ seen.add(key);
177
158
  if (!isStream(node))
178
159
  validateSiblingNames(node.name, node.children);
179
160
  }
@@ -210,8 +191,8 @@ function writeHeader(dv, p) {
210
191
  dv.setUint32(HEADER_FIELD.miniStreamCutoff, MINI_STREAM_CUTOFF, true);
211
192
  dv.setUint32(HEADER_FIELD.firstMiniFatSector, p.miniFatStart, true);
212
193
  dv.setUint32(HEADER_FIELD.miniFatSectors, p.miniFatSectors, true);
213
- dv.setUint32(HEADER_FIELD.firstDifatSector, p.difatSectors > 0 ? p.difatStart : ENDOFCHAIN, true);
214
- dv.setUint32(HEADER_FIELD.difatSectors, p.difatSectors, true);
194
+ dv.setUint32(HEADER_FIELD.firstDifatSector, ENDOFCHAIN, true);
195
+ dv.setUint32(HEADER_FIELD.difatSectors, 0, true);
215
196
  for (let i = 0; i < HEADER_DIFAT_SLOTS; i++) {
216
197
  dv.setUint32(HEADER_FIELD.difat + i * 4, i < p.fatSectors ? p.fatStart + i : FREESECT, true);
217
198
  }
package/dist/vba/cfb.js CHANGED
@@ -12,6 +12,8 @@ export class CompoundFile {
12
12
  #fat;
13
13
  #miniFat;
14
14
  #dir;
15
+ #rootIndex;
16
+ #root;
15
17
  #miniStream;
16
18
  constructor(buf) {
17
19
  this.#buf = buf;
@@ -44,15 +46,16 @@ export class CompoundFile {
44
46
  this.#fat = this.#readFat(fatSectorIds);
45
47
  this.#miniFat = this.#readChainValues(firstMiniFatSector);
46
48
  this.#dir = this.#readDirectory(firstDirSector);
47
- const root = this.#dir.find((e) => e.type === TYPE_ROOT);
49
+ this.#rootIndex = this.#dir.findIndex((e) => e.type === TYPE_ROOT);
50
+ const root = this.#dir[this.#rootIndex];
48
51
  if (!root)
49
52
  throw new VbaParseError('compound file has no root storage entry');
53
+ this.#root = root;
50
54
  this.#miniStream = this.#readViaFat(root.startSector, root.size);
51
55
  }
52
56
  readStream(path) {
53
- const rootIdx = this.#rootIndex();
54
- const seen = new Set([rootIdx]);
55
- let firstChild = this.#dir[rootIdx].child;
57
+ const seen = new Set([this.#rootIndex]);
58
+ let firstChild = this.#root.child;
56
59
  for (const [depth, segment] of path.entries()) {
57
60
  const stream = depth === path.length - 1;
58
61
  const entry = this.#childIndices(firstChild, seen, depth)
@@ -68,14 +71,7 @@ export class CompoundFile {
68
71
  return undefined;
69
72
  }
70
73
  tree() {
71
- const rootIdx = this.#rootIndex();
72
- return this.#buildSiblings(this.#dir[rootIdx].child, new Set([rootIdx]));
73
- }
74
- #rootIndex() {
75
- const rootIdx = this.#dir.findIndex((e) => e.type === TYPE_ROOT);
76
- if (rootIdx < 0)
77
- throw new VbaParseError('compound file has no root storage entry');
78
- return rootIdx;
74
+ return this.#buildSiblings(this.#root.child, new Set([this.#rootIndex]));
79
75
  }
80
76
  #buildSiblings(firstChild, seen, depth = 0) {
81
77
  if (depth > MAX_TREE_DEPTH)
@@ -8,12 +8,12 @@ import { CompoundFile } from './cfb.js';
8
8
  import { decoderForCodePage, encoderForCodePage } from './codepage.js';
9
9
  import { dirRecord, dirRecords, REC_MODULE_NAME, REC_MODULE_STREAMNAME, REC_MODULE_TERMINATOR, REC_MODULES_COUNT, REC_REFERENCE_NAME, REC_REFERENCE_NAME_UNICODE, REC_REFERENCE_REGISTERED, } from './dir-records.js';
10
10
  import { VbaAuthorError, VbaParseError } from './errors.js';
11
- import { compressContainer, decompressContainer } from './ms-ovba.js';
11
+ import { compressContainer } from './ms-ovba.js';
12
12
  import { DIR_PATH, parseVbaProjectIn, PROJECT_PATH, PROJECTWM_PATH, VBA_STORAGE } from './project.js';
13
13
  import { validateReferenceName } from './vba-encoding.js';
14
14
  export function removeVbaModule(bin, name) {
15
15
  const cfb = new CompoundFile(bin);
16
- const project = parseVbaProjectIn(cfb);
16
+ const { project, dir } = parseVbaProjectIn(cfb);
17
17
  const nameKey = name.toUpperCase();
18
18
  const module = project.modules.find((m) => m.name.toUpperCase() === nameKey);
19
19
  if (!module)
@@ -23,10 +23,7 @@ export function removeVbaModule(bin, name) {
23
23
  'primitive cannot verify');
24
24
  }
25
25
  const decoder = decoderForCodePage(project.codePage);
26
- const dirCompressed = cfb.readStream(DIR_PATH);
27
- if (!dirCompressed)
28
- throw new VbaParseError("VBA project has no 'dir' stream");
29
- const patchedDir = removeModuleDirRecord(decompressContainer(dirCompressed), module.streamName, decoder);
26
+ const patchedDir = removeModuleDirRecord(dir, module.streamName, decoder);
30
27
  const replacements = [{ path: DIR_PATH, data: compressContainer(patchedDir) }];
31
28
  const projectText = cfb.readStream(PROJECT_PATH);
32
29
  if (projectText) {
@@ -90,12 +87,10 @@ function normalizeReference(ref) {
90
87
  export function addVbaReference(bin, ref) {
91
88
  const normalized = normalizeReference(ref);
92
89
  const cfb = new CompoundFile(bin);
93
- const encode = encoderForCodePage(parseVbaProjectIn(cfb).codePage);
94
- const dirCompressed = cfb.readStream(DIR_PATH);
95
- if (!dirCompressed)
96
- throw new VbaParseError("VBA project has no 'dir' stream");
90
+ const { project, dir } = parseVbaProjectIn(cfb);
91
+ const encode = encoderForCodePage(project.codePage);
97
92
  const records = buildReferenceDirRecords(normalized, encode);
98
- const patchedDir = insertReferenceDirRecords(decompressContainer(dirCompressed), records);
93
+ const patchedDir = insertReferenceDirRecords(dir, records);
99
94
  return writeCompoundFile(replaceStreams(cfb.tree(), [{ path: DIR_PATH, data: compressContainer(patchedDir) }]));
100
95
  }
101
96
  function buildReferenceDirRecords(ref, encode) {
@@ -54,6 +54,10 @@ export declare function parseVbaProject(bin: Uint8Array, maxOutput?: number): Vb
54
54
  * Both project editors parse fail-closed first and then need the container itself, and each was
55
55
  * building a second `CompoundFile` over the same bytes: a second walk of the DIFAT, FAT, mini-FAT,
56
56
  * directory and mini-stream, which is roughly double the cost and double the peak allocation of every
57
- * edit. Opening it once and handing it in is the whole of the fix.
57
+ * edit. Opening it once and handing it in is the whole of the fix. The decompressed `dir` stream comes
58
+ * back beside the project, so an editor patches the bytes the parse validated and budgeted.
58
59
  */
59
- export declare function parseVbaProjectIn(cfb: CompoundFile, maxOutput?: number): VbaProject;
60
+ export declare function parseVbaProjectIn(cfb: CompoundFile, maxOutput?: number): {
61
+ project: VbaProject;
62
+ dir: Uint8Array;
63
+ };
@@ -25,7 +25,7 @@ export const DIR_PATH = [VBA_STORAGE, 'dir'];
25
25
  export const PROJECT_PATH = ['PROJECT'];
26
26
  export const PROJECTWM_PATH = ['PROJECTwm'];
27
27
  export function parseVbaProject(bin, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
28
- return parseVbaProjectIn(new CompoundFile(bin), maxOutput);
28
+ return parseVbaProjectIn(new CompoundFile(bin), maxOutput).project;
29
29
  }
30
30
  export function parseVbaProjectIn(cfb, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
31
31
  const budget = new DecompressionBudget(maxOutput);
@@ -80,7 +80,7 @@ export function parseVbaProjectIn(cfb, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
80
80
  source: readModuleSource(cfb, streamName, m.offset, decoder, budget),
81
81
  };
82
82
  });
83
- return { codePage, modules };
83
+ return { project: { codePage, modules }, dir };
84
84
  }
85
85
  function readModuleSource(cfb, streamName, textOffset, decoder, budget) {
86
86
  const stream = cfb.readStream([VBA_STORAGE, streamName]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shbernal/ts-xlsx",
3
- "version": "3.2.2-next.24.g38fae0e",
3
+ "version": "3.2.2-next.26.gf63f249",
4
4
  "description": "A TypeScript-first library for reading and writing xlsx (OOXML) spreadsheets.",
5
5
  "keywords": [
6
6
  "csv",