fix(export): neutralize spreadsheet formulas in CSV exports
Typed-in values (colony codes, batch numbers, honey types) starting with = + - @ TAB or CR were written as-is and ran as formulas (HYPERLINK, DDE) when the export was opened in Excel or LibreOffice. Such text cells now get a leading apostrophe; numbers keep their sign. Carriage returns are quoted as well. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
f1dc0de3e2
commit
61e32474e4
2 files changed
+32
-3
No files matched your search
@@ -0,0 +1,21 @@
|
|||||||
|
import { toCsvField } from './export.service';
|
||||||
|
|
||||||
|
describe('toCsvField', () => {
|
||||||
|
it('neutralizes cells that a spreadsheet would run as a formula', () => {
|
||||||
|
expect(toCsvField('=HYPERLINK("http://evil","x")')).toBe(
|
||||||
|
`"'=HYPERLINK(""http://evil"",""x"")"`,
|
||||||
|
);
|
||||||
|
expect(toCsvField('+1+1')).toBe("'+1+1");
|
||||||
|
expect(toCsvField('-2')).toBe("'-2");
|
||||||
|
expect(toCsvField('@SUM(A1)')).toBe("'@SUM(A1)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps numbers, including negative ones, as numbers', () => {
|
||||||
|
expect(toCsvField(-2.5)).toBe('-2.5');
|
||||||
|
expect(toCsvField(0)).toBe('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('quotes carriage returns too', () => {
|
||||||
|
expect(toCsvField('a\rb')).toBe('"a\rb"');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -71,12 +71,20 @@ function stringifyCsvValue(value: unknown): string {
|
|||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toCsvField(value: unknown): string {
|
// A text cell starting with one of these is run as a formula by Excel and
|
||||||
|
// LibreOffice (=HYPERLINK(...), DDE). Typed-in values such as a colony code
|
||||||
|
// are prefixed with ' so they show as text; real numbers are not strings
|
||||||
|
// and keep their sign.
|
||||||
|
const FORMULA_TRIGGER = /^[=+\-@\t\r]/;
|
||||||
|
|
||||||
|
export function toCsvField(value: unknown): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const text = stringifyCsvValue(value);
|
const raw = stringifyCsvValue(value);
|
||||||
if (/[",\n]/.test(text)) {
|
const text =
|
||||||
|
typeof value === 'string' && FORMULA_TRIGGER.test(raw) ? `'${raw}` : raw;
|
||||||
|
if (/[",\r\n]/.test(text)) {
|
||||||
return `"${text.replace(/"/g, '""')}"`;
|
return `"${text.replace(/"/g, '""')}"`;
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
Reference in new issue
Block a user