From fd39cab8458fd442831051b5d816be554f5ac2e4 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 15 Mar 2026 20:58:08 +0100 Subject: [PATCH] fix: prevent sheet name too long in excel export --- .../excel/__tests__/excel.service.test.ts | 37 +++++++++++++++++++ .../src/api-data/excel/excel.service.ts | 15 +++++++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/api-data/excel/__tests__/excel.service.test.ts diff --git a/apps/server/src/api-data/excel/__tests__/excel.service.test.ts b/apps/server/src/api-data/excel/__tests__/excel.service.test.ts new file mode 100644 index 000000000..b8e0e18fc --- /dev/null +++ b/apps/server/src/api-data/excel/__tests__/excel.service.test.ts @@ -0,0 +1,37 @@ +import xlsx from 'xlsx'; + +import { demoDb } from '../../../models/demoProject.js'; +import { generateExcelFile } from '../excel.service.js'; + +describe('generateExcelFile()', () => { + it('sanitises long worksheet names to an Excel-compatible value', () => { + const buffer = generateExcelFile( + { + ...demoDb.rundowns.default, + title: 'This is a very long name with many characters and weird things: like [Main]/?*', + }, + demoDb.customFields, + ); + + const workbook = xlsx.read(buffer, { type: 'buffer' }); + const worksheetName = workbook.SheetNames[0]; + + expect(worksheetName).toBeDefined(); + expect(worksheetName.length).toBeLessThanOrEqual(31); + expect(worksheetName).not.toMatch(/[:\\/?*3[\]]/); + }); + + it('falls back to default worksheet name when title is fully invalid', () => { + const buffer = generateExcelFile( + { + ...demoDb.rundowns.default, + title: '[]:*?/\\', + }, + demoDb.customFields, + ); + + const workbook = xlsx.read(buffer, { type: 'buffer' }); + + expect(workbook.SheetNames[0]).toBe('Rundown'); + }); +}); diff --git a/apps/server/src/api-data/excel/excel.service.ts b/apps/server/src/api-data/excel/excel.service.ts index b0085a150..4333c738e 100644 --- a/apps/server/src/api-data/excel/excel.service.ts +++ b/apps/server/src/api-data/excel/excel.service.ts @@ -21,6 +21,18 @@ import { rundownToTabular } from './excel.utils.js'; // we keep the excel data in memory to allow the flow upload -> preview let excelData: WorkBook = xlsx.utils.book_new(); +const maxWorksheetNameLength = 31; +const invalidWorksheetCharsRegex = /[:\\/?*[\]]/g; + +function getValidWorksheetName(title: string): string { + const sanitisedTitle = title.replaceAll(invalidWorksheetCharsRegex, ' ').trim().replace(/\s+/g, ' '); + + const withFallback = sanitisedTitle.length > 0 ? sanitisedTitle : 'Rundown'; + const truncatedTitle = withFallback.slice(0, maxWorksheetNameLength).trim(); + + return truncatedTitle.length > 0 ? truncatedTitle : 'Rundown'; +} + /** * Receives and parses an excel file * The file is deleted after being read @@ -93,7 +105,8 @@ export function generateExcelFile(rundown: Rundown, customFields: CustomFields): const workbook = xlsx.utils.book_new(); const worksheet = xlsx.utils.aoa_to_sheet(rundownToTabular(rundown, customFields)); - xlsx.utils.book_append_sheet(workbook, worksheet, rundown.title || 'Rundown'); + const worksheetName = getValidWorksheetName(rundown.title || 'Rundown'); + xlsx.utils.book_append_sheet(workbook, worksheet, worksheetName); return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' }); }