From d247ec9d1b01fa7bb16d8f594e06780baf95329e Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Wed, 30 Sep 2026 21:34:13 +0000 Subject: [PATCH] Gospel x Internet CSV export: a value starting with TAB or CR is neutralised too, and the model check asserts it --- .../gospel-internet/scripts/check-gospel-internet-model.mjs | 6 ++++++ plugin-designs/gospel-internet/src/model.js | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs b/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs index 4ad29d8a5..d9f1a4ea0 100644 --- a/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs +++ b/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs @@ -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 diff --git a/plugin-designs/gospel-internet/src/model.js b/plugin-designs/gospel-internet/src/model.js index 2144076ce..ac45097af 100644 --- a/plugin-designs/gospel-internet/src/model.js +++ b/plugin-designs/gospel-internet/src/model.js @@ -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 }