Where This Parser Disagrees With RFC 4180, Measured
RFC 4180 is short enough to implement exactly, and this parser is close to it. Close is where the interesting failures live. I ran inputs through the function that actually ships — extracted from the deployed page's own <script>, not a copy — and every output below is pasted verbatim from that run.
How to re-run this
The app logic is inline in apps/csvformat/public/index.html. The harness loads it unmodified against a DOM stub, so there is no fork between what is fuzzed and what is published:
// from scripts/fuzz/
const { loadApp } = require("./load");
const { api } = loadApp("csvformat", ["csvToJson", "csvToSql"]);
const toJson = s => { api.__setInput("in", s); api.csvToJson(); return api.__readOutput("out"); };
const toSql = s => { api.__setInput("in", s); api.csvToSql(); return api.__readOutput("out"); };
console.log(toJson(`a,b\n"1,2",3`));
What it gets right
The parts that matter most are correct, and worth stating before the complaints. Quoted delimiters, escaped quotes, and CRLF all behave to spec:
| Input | JSON output | |
|---|---|---|
a,b ⏎ "1,2",3 | {"a":"1,2","b":"3"} | ✅ quoted comma |
a ⏎ "say ""hi""" | {"a":"say \"hi\""} | ✅ doubled quote |
a,b ⏎ 1,2 (CRLF) | {"a":"1","b":"2"} | ✅ \r dropped |
The six deviations
1. Cells are trimmed — including inside quotes
parseCsv finishes with clean.map(r => r.map(c => c.trim())), applied to every cell after parsing. RFC 4180 is explicit that whitespace inside a quoted field is part of the field.
| Input | Output | RFC 4180 says |
|---|---|---|
" x " | x | " x " — preserve |
"\tx\t" | x | \tx\t — a tab is not whitespace to strip |
" " (spaces only) | "" | " " — three spaces |
The tab case is the one that bites. If a cell holds pre-formatted text — a code snippet, a TSV column, a fixed-width excerpt — converting it through this tool flattens it, and flattens it identically on every run, so it looks like the source was always flat.
There is no flag for this. I checked the comparison case rather than assume it: PapaParse 5.7.0 with default options preserves the same input ([["a","b"],[" x "," y "],[" p ","q"]]), so the trimming here is a deliberate local choice, not what "a normal CSV library" does. If exact whitespace matters, that is the library to reach for; if you want this tool, trim awareness is on you.
2. A semicolon file becomes one column
No delimiter sniffing, no delimiter option. a;b;c ⏎ 1;2;3 returns:
[{ "a;b;c": "1;2;3" }]
One key, one value, no warning. EU-locale spreadsheets export semicolon-delimited by default, so this is a common input, not an edge case. The stats line does not change shape either — it still reports a row count, which is what makes it easy to miss.
3. Ragged rows produce SQL that will not parse
Input a,b,c ⏎ 1,2 — three headers, two values. The emitted statement is:
INSERT INTO records (a, b, c) VALUES (1, 2);
-- WARNING: 1 row(s) did not match the header column count
Three columns, two values. That is a syntax error in PostgreSQL, MySQL, and SQLite, so the warning is doing real work — but note what it means for the file as a whole: the statement is not executable, so you cannot paste this and fix one row. The JSON path is gentler and pads the short row with "", so the same input gives you a usable object and a broken script. Pick your output format by how you intend to check it.
4. Numeric coercion is lossy, and quoting does not protect you
The SQL emitter decides per value whether to quote, using a numeric test rather than the parsed field's origin. Measured:
| Input cell | Emitted | Lost |
|---|---|---|
01234 | 1234 | leading zero |
"01234" (quoted in source) | 1234 | quoting did not help |
9007199254740993 | 9007199254740992 | last digit — Number() precision |
1.50 | 1.5 | significant-zero intent |
+447700900123 | 447700900123 | leading + |
The quoted case is the important one. In CSV, quoting a field is exactly how a producer says "this is text, not a number" — it is why Excel writes "01234". That signal is discarded because trimming has already removed the quotes before the numeric test sees the value. Zip codes, SKU codes, phone numbers, IBANs, and any ID above 2^53 all arrive as quoted strings and all come out as numbers.
The JSON path is unaffected: "01" stays the string "01". The loss is specific to the SQL emitter.
5. NULL and the empty cell
| Input | Emitted | Note |
|---|---|---|
NULL (bare text) | 'NULL' | quoted — safe, arguably correct |
| empty cell | '' | empty string, not NULL |
Treating a literal NULL as a string is the defensible choice — a CSV cell containing the four letters N-U-L-L is usually data. But an empty cell becoming '' rather than NULL is a silent semantic commitment: if your target table has NOT NULL defaults or you intended a null insert, '' will load without complaint and change meaning. There is no option for this; post-process if it matters.
6. Header handling
A header containing a space is emitted double-quoted — "first name" — which is correct for Postgres and SQLite and wrong for default MySQL, where double quotes mean a string literal unless ANSI_QUOTES is on. Identifiers are not otherwise escaped, so a header containing a double quote or a backtick is emitted raw.
What I would do with this
- JSON out is the safe path. No coercion, no SQL syntax, quoting preserved except for trimming.
- Before SQL out, look at any cell that is digits with a leading zero, a
+, or more than 15 characters. Those four cases are where the emitter changes your data. - Never feed it a semicolon file without replacing
;with,first, and check the row count against your column count afterwards — one key in the JSON means sniffing failed, not that your file has one column. - If exact whitespace matters, this tool is the wrong tool, and no setting will make it right.
The deviations are consistent and small, which is what makes them worth documenting instead of fixing quietly: every one of them produces plausible-looking output.