mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix(preview): normalize entry names the way ExcelJS sees them, skip defined names, pin fflate 0.8.3
Admission checked ZIP entry names as stored, but JSZip (inside ExcelJS) resolves `.`, `..` and empty segments on load, and ExcelJS strips one leading `/` and matches worksheets with an unanchored pattern. Names like `/xl/worksheets/sheet1.xml` or `xl/worksheets/sheet1.xml.x` skipped every counter. Admission now computes the name ExcelJS will see for each entry, refuses two entries that resolve to the same name, keys the rebuilt archive on it, and picks the worksheet/styles counters from it. ExcelJS's DefinedNames model setter expands every range into one object per cell. The preview never shows defined names, so the worker stubs `_definedNames.model` before load. Pin fflate to 0.8.3 (GHSA-px8p-9vwx-vf98) and refresh SPREADSHEET_ASSET_VERSION.
This commit is contained in:
@@ -625,3 +625,74 @@ async function emptyRowsWorkbook(extraRows: string): Promise<ArrayBuffer> {
|
||||
entries['xl/worksheets/sheet1.xml'] = fflate.strToU8(patched);
|
||||
return toArrayBuffer(fflate.zipSync(entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* A one-cell workbook whose sheet carries a merge one cell over the per-sheet
|
||||
* cap, stored under `entryName` instead of `xl/worksheets/sheet1.xml`. JSZip
|
||||
* (inside ExcelJS) resolves `.`, `..` and empty segments, and ExcelJS strips one
|
||||
* leading `/` and matches worksheets with an unanchored pattern, so each of
|
||||
* these names still reaches ExcelJS as a worksheet.
|
||||
*/
|
||||
async function renamedOversizedSheet(entryName: string, relTarget?: string): Promise<ArrayBuffer> {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.addWorksheet('Data').getCell('A1').value = 'one';
|
||||
const entries = fflate.unzipSync(new Uint8Array(await workbook.xlsx.writeBuffer()));
|
||||
const sheet = fflate.strFromU8(entries['xl/worksheets/sheet1.xml']);
|
||||
const patched = sheet.replace(
|
||||
'</sheetData>',
|
||||
'</sheetData><mergeCells count="1"><mergeCell ref="A1:A100001"/></mergeCells>'
|
||||
);
|
||||
expect(patched).not.toBe(sheet);
|
||||
delete entries['xl/worksheets/sheet1.xml'];
|
||||
entries[entryName] = fflate.strToU8(patched);
|
||||
if (relTarget) {
|
||||
const rels = fflate.strFromU8(entries['xl/_rels/workbook.xml.rels']);
|
||||
const retargeted = rels.replace('Target="worksheets/sheet1.xml"', `Target="${relTarget}"`);
|
||||
expect(retargeted).not.toBe(rels);
|
||||
entries['xl/_rels/workbook.xml.rels'] = fflate.strToU8(retargeted);
|
||||
}
|
||||
return toArrayBuffer(fflate.zipSync(entries));
|
||||
}
|
||||
|
||||
describe('spreadsheet preview worker: entry names as ExcelJS sees them', () => {
|
||||
it.each([
|
||||
['/xl/worksheets/sheet1.xml', undefined],
|
||||
['xl/./worksheets/sheet1.xml', undefined],
|
||||
['xl//worksheets/sheet1.xml', undefined],
|
||||
['xl/xl/worksheets/sheet1.xml', 'xl/worksheets/sheet1.xml'],
|
||||
['xl/worksheets/sheet1.xml.x', 'worksheets/sheet1.xml.x'],
|
||||
] as const)(
|
||||
'counts a sheet stored as %s and refuses it before ExcelJS loads',
|
||||
async (entryName, relTarget) => {
|
||||
const bytes = await renamedOversizedSheet(entryName, relTarget);
|
||||
// The fixture is real: unguarded ExcelJS parses this entry as the sheet.
|
||||
const direct = new ExcelJS.Workbook();
|
||||
await direct.xlsx.load(bytes.slice(0));
|
||||
expect(direct.worksheets[0]?.model.merges).toContain('A1:A100001');
|
||||
|
||||
const harness = createHarness();
|
||||
await harness.send({ type: 'load', bytes });
|
||||
expect(harness.messages.at(-1)).toMatchObject({ type: 'error', code: 'cell-limit' });
|
||||
expect(harness.imports.some((url) => url.includes('exceljs'))).toBe(false);
|
||||
},
|
||||
30_000
|
||||
);
|
||||
});
|
||||
|
||||
describe('spreadsheet preview worker: defined names', () => {
|
||||
// ExcelJS's DefinedNames model setter creates one object per cell of every
|
||||
// range; a whole-sheet name exhausted a 4 GB heap. The preview never shows
|
||||
// defined names, so they are not expanded at all.
|
||||
it('loads a workbook with a defined name without expanding its range', async () => {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.addWorksheet('Data').getCell('A1').value = 'one';
|
||||
workbook.definedNames.add('Data!$A$1:$J$10', 'Block');
|
||||
const bytes = await writeWorkbook(workbook);
|
||||
expect(fflate.strFromU8(fflate.unzipSync(new Uint8Array(bytes))['xl/workbook.xml'])).toContain('Block');
|
||||
|
||||
const harness = createHarness();
|
||||
const metadata = await loadMetadata(harness, bytes);
|
||||
expect(metadata.sheets[0]).toMatchObject({ name: 'Data', rows: 1, cols: 1 });
|
||||
expect(harness.peek('Object.keys(workbook.definedNames.matrixMap).length')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user