Gospel x Internet CSV export: a value starting with TAB or CR is neutralised too, and the model check asserts it

This commit is contained in:
Doxa Maps 2026-09-30 21:34:13 +00:00
parent 56b53c2a92
commit d247ec9d1b
2 changed files with 9 additions and 1 deletions

View file

@ -111,6 +111,12 @@ const csv = M.buildCsv(
const lines = csv.split('\r\n')
ok(lines[0] === 'A,B' && lines[lines.length - 1].startsWith('# test') && (POISON ? false : true), 'CSV header first, provenance last')
ok(lines[1].startsWith('"\'=HYPERLINK') && lines[1].endsWith('"Arabic, Mesopotamian"'), 'CSV neutralises = and quotes an embedded comma')
// A spreadsheet can strip a leading TAB or CR and then read the = after it as a formula (OWASP's list), so those
// two lead characters are neutralised too, the same as the library's useCsvExport.
const tabCell = M.buildCsv([{ a: '\t=cmd' }], [{ key: 'a', header: 'A' }]).split('\r\n')[1]
ok(tabCell === "'\t=cmd", `CSV neutralises a leading TAB (got ${JSON.stringify(tabCell)})`)
const crCell = M.buildCsv([{ a: '\r=cmd' }], [{ key: 'a', header: 'A' }]).slice('A\r\n'.length)
ok(crCell === '"\'\r=cmd"', `CSV neutralises a leading CR, then quotes it (got ${JSON.stringify(crCell)})`)
// 9. platforms: the preset is "nothing in own language, 60%+ online" (the 220), and the app chart counts
// each COUNTRY once, with countries outside Apple's feed kept as "not measured", never dropped

View file

@ -301,7 +301,9 @@ export function sortRows(rows, { key, dir = 1 } = {}) {
export function buildCsv(rows, columns, { provenance = '' } = {}) {
const esc = (v) => {
let s = v === null || v === undefined ? '' : String(v)
if (/^[=+\-@]/.test(s)) s = "'" + s
// A leading = + - @ TAB or CR is neutralised: a spreadsheet can strip a leading TAB/CR and then read the =
// after it as a formula (OWASP's list). The same guard as the library's useCsvExport.
if (/^[=+\-@\t\r]/.test(s)) s = "'" + s
if (/[",\n\r]/.test(s)) s = '"' + s.replace(/"/g, '""') + '"'
return s
}