Skip to content

Commit a03fab1

Browse files
authored
fix(encoding/csv): escape cells containing newlines (LFs) (#3128)
1 parent ec2f37c commit a03fab1

File tree

2 files changed

+42
-46
lines changed

2 files changed

+42
-46
lines changed

encoding/csv.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export {
2525
export type { ReadOptions } from "./csv/_io.ts";
2626

2727
const QUOTE = '"';
28-
export const NEWLINE = "\r\n";
28+
const LF = "\n";
29+
const CRLF = "\r\n";
2930

3031
export class StringifyError extends Error {
3132
override readonly name = "StringifyError";
@@ -40,7 +41,7 @@ function getEscapedString(value: unknown, sep: string): string {
4041

4142
// Is regex.test more performant here? If so, how to dynamically create?
4243
// https://stackoverflow.com/questions/3561493/
43-
if (str.includes(sep) || str.includes(NEWLINE) || str.includes(QUOTE)) {
44+
if (str.includes(sep) || str.includes(LF) || str.includes(QUOTE)) {
4445
return `${QUOTE}${str.replaceAll(QUOTE, `${QUOTE}${QUOTE}`)}${QUOTE}`;
4546
}
4647

@@ -291,7 +292,7 @@ export function stringify(
291292
data: DataItem[],
292293
{ headers = true, separator: sep = ",", columns = [] }: StringifyOptions = {},
293294
): string {
294-
if (sep.includes(QUOTE) || sep.includes(NEWLINE)) {
295+
if (sep.includes(QUOTE) || sep.includes(CRLF)) {
295296
const message = [
296297
"Separator cannot include the following strings:",
297298
' - U+0022: Quotation mark (")',
@@ -307,15 +308,15 @@ export function stringify(
307308
output += normalizedColumns
308309
.map((column) => getEscapedString(column.header, sep))
309310
.join(sep);
310-
output += NEWLINE;
311+
output += CRLF;
311312
}
312313

313314
for (const item of data) {
314315
const values = getValuesFromItem(item, normalizedColumns);
315316
output += values
316317
.map((value) => getEscapedString(value, sep))
317318
.join(sep);
318-
output += NEWLINE;
319+
output += CRLF;
319320
}
320321

321322
return output;

encoding/csv_test.ts

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
66

77
import { assertEquals, assertThrows } from "../testing/asserts.ts";
8-
import {
9-
NEWLINE,
10-
parse,
11-
ParseError,
12-
stringify,
13-
StringifyError,
14-
} from "./csv.ts";
8+
import { parse, ParseError, stringify, StringifyError } from "./csv.ts";
9+
10+
const CRLF = "\r\n";
1511

1612
Deno.test({
1713
name: "parse",
@@ -864,7 +860,7 @@ Deno.test({
864860
fn() {
865861
const columns: string[] = [];
866862
const data: string[][] = [];
867-
const output = NEWLINE;
863+
const output = CRLF;
868864
assertEquals(stringify(data, { columns }), output);
869865
},
870866
},
@@ -887,7 +883,7 @@ Deno.test({
887883
fn() {
888884
const columns = ["a"];
889885
const data: string[][] = [];
890-
const output = `a${NEWLINE}`;
886+
const output = `a${CRLF}`;
891887
assertEquals(stringify(data, { columns }), output);
892888
},
893889
},
@@ -911,7 +907,7 @@ Deno.test({
911907
fn() {
912908
const columns = [0, 1];
913909
const data = [["foo", "bar"], ["baz", "qux"]];
914-
const output = `0\r1${NEWLINE}foo\rbar${NEWLINE}baz\rqux${NEWLINE}`;
910+
const output = `0\r1${CRLF}foo\rbar${CRLF}baz\rqux${CRLF}`;
915911
const options = { separator: "\r", columns };
916912
assertEquals(stringify(data, options), output);
917913
},
@@ -924,7 +920,7 @@ Deno.test({
924920
fn() {
925921
const columns = [0, 1];
926922
const data = [["foo", "bar"], ["baz", "qux"]];
927-
const output = `0\n1${NEWLINE}foo\nbar${NEWLINE}baz\nqux${NEWLINE}`;
923+
const output = `0\n1${CRLF}foo\nbar${CRLF}baz\nqux${CRLF}`;
928924
const options = { separator: "\n", columns };
929925
assertEquals(stringify(data, options), output);
930926
},
@@ -936,7 +932,7 @@ Deno.test({
936932
fn() {
937933
const columns = [1];
938934
const data = [{ 1: 1 }, { 1: 2 }];
939-
const output = `1${NEWLINE}1${NEWLINE}2${NEWLINE}`;
935+
const output = `1${CRLF}1${CRLF}2${CRLF}`;
940936
assertEquals(stringify(data, { columns }), output);
941937
},
942938
},
@@ -948,7 +944,7 @@ Deno.test({
948944
fn() {
949945
const columns = [{ header: "Value", prop: "value" }];
950946
const data = [{ value: "foo" }, { value: "bar" }];
951-
const output = `foo${NEWLINE}bar${NEWLINE}`;
947+
const output = `foo${CRLF}bar${CRLF}`;
952948
const options = { headers: false, columns };
953949
assertEquals(stringify(data, options), output);
954950
},
@@ -960,7 +956,7 @@ Deno.test({
960956
fn() {
961957
const columns = [1];
962958
const data = [["key", "foo"], ["key", "bar"]];
963-
const output = `1${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
959+
const output = `1${CRLF}foo${CRLF}bar${CRLF}`;
964960
assertEquals(stringify(data, { columns }), output);
965961
},
966962
},
@@ -972,7 +968,7 @@ Deno.test({
972968
fn() {
973969
const columns = [[1]];
974970
const data = [{ 1: 1 }, { 1: 2 }];
975-
const output = `1${NEWLINE}1${NEWLINE}2${NEWLINE}`;
971+
const output = `1${CRLF}1${CRLF}2${CRLF}`;
976972
assertEquals(stringify(data, { columns }), output);
977973
},
978974
},
@@ -983,7 +979,7 @@ Deno.test({
983979
fn() {
984980
const columns = [[1]];
985981
const data = [["key", "foo"], ["key", "bar"]];
986-
const output = `1${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
982+
const output = `1${CRLF}foo${CRLF}bar${CRLF}`;
987983
assertEquals(stringify(data, { columns }), output);
988984
},
989985
},
@@ -995,7 +991,7 @@ Deno.test({
995991
fn() {
996992
const columns = [[1, 1]];
997993
const data = [["key", ["key", "foo"]], ["key", ["key", "bar"]]];
998-
const output = `1${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
994+
const output = `1${CRLF}foo${CRLF}bar${CRLF}`;
999995
assertEquals(stringify(data, { columns }), output);
1000996
},
1001997
},
@@ -1006,7 +1002,7 @@ Deno.test({
10061002
fn() {
10071003
const columns = ["value"];
10081004
const data = [{ value: "foo" }, { value: "bar" }];
1009-
const output = `value${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
1005+
const output = `value${CRLF}foo${CRLF}bar${CRLF}`;
10101006
assertEquals(stringify(data, { columns }), output);
10111007
},
10121008
},
@@ -1017,7 +1013,7 @@ Deno.test({
10171013
fn() {
10181014
const columns = [["value"]];
10191015
const data = [{ value: "foo" }, { value: "bar" }];
1020-
const output = `value${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
1016+
const output = `value${CRLF}foo${CRLF}bar${CRLF}`;
10211017
assertEquals(stringify(data, { columns }), output);
10221018
},
10231019
},
@@ -1028,7 +1024,7 @@ Deno.test({
10281024
fn() {
10291025
const columns = [["msg", "value"]];
10301026
const data = [{ msg: { value: "foo" } }, { msg: { value: "bar" } }];
1031-
const output = `value${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
1027+
const output = `value${CRLF}foo${CRLF}bar${CRLF}`;
10321028
assertEquals(stringify(data, { columns }), output);
10331029
},
10341030
},
@@ -1044,7 +1040,7 @@ Deno.test({
10441040
},
10451041
];
10461042
const data = [{ msg: { value: "foo" } }, { msg: { value: "bar" } }];
1047-
const output = `Value${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
1043+
const output = `Value${CRLF}foo${CRLF}bar${CRLF}`;
10481044
assertEquals(stringify(data, { columns }), output);
10491045
},
10501046
},
@@ -1057,7 +1053,7 @@ Deno.test({
10571053
const columns = [0];
10581054
const data = [[{ value: "foo" }], [{ value: "bar" }]];
10591055
const output =
1060-
`0${NEWLINE}"{""value"":""foo""}"${NEWLINE}"{""value"":""bar""}"${NEWLINE}`;
1056+
`0${CRLF}"{""value"":""foo""}"${CRLF}"{""value"":""bar""}"${CRLF}`;
10611057
assertEquals(stringify(data, { columns }), output);
10621058
},
10631059
},
@@ -1072,7 +1068,7 @@ Deno.test({
10721068
[[{ value: "baz" }, { value: "qux" }]],
10731069
];
10741070
const output =
1075-
`0${NEWLINE}"[{""value"":""foo""},{""value"":""bar""}]"${NEWLINE}"[{""value"":""baz""},{""value"":""qux""}]"${NEWLINE}`;
1071+
`0${CRLF}"[{""value"":""foo""},{""value"":""bar""}]"${CRLF}"[{""value"":""baz""},{""value"":""qux""}]"${CRLF}`;
10761072
assertEquals(stringify(data, { columns }), output);
10771073
},
10781074
},
@@ -1084,7 +1080,7 @@ Deno.test({
10841080
const columns = [0];
10851081
const data = [[["foo", "bar"]], [["baz", "qux"]]];
10861082
const output =
1087-
`0${NEWLINE}"[""foo"",""bar""]"${NEWLINE}"[""baz"",""qux""]"${NEWLINE}`;
1083+
`0${CRLF}"[""foo"",""bar""]"${CRLF}"[""baz"",""qux""]"${CRLF}`;
10881084
assertEquals(stringify(data, { columns }), output);
10891085
},
10901086
},
@@ -1097,7 +1093,7 @@ Deno.test({
10971093
const columns = [0];
10981094
const data = [[["foo", "bar"]], [["baz", "qux"]]];
10991095
const output =
1100-
`0${NEWLINE}"[""foo"",""bar""]"${NEWLINE}"[""baz"",""qux""]"${NEWLINE}`;
1096+
`0${CRLF}"[""foo"",""bar""]"${CRLF}"[""baz"",""qux""]"${CRLF}`;
11011097
const options = { separator: "\t", columns };
11021098
assertEquals(stringify(data, options), output);
11031099
},
@@ -1109,7 +1105,7 @@ Deno.test({
11091105
fn() {
11101106
const columns = [0];
11111107
const data = [[], []];
1112-
const output = `0${NEWLINE}${NEWLINE}${NEWLINE}`;
1108+
const output = `0${CRLF}${CRLF}${CRLF}`;
11131109
assertEquals(stringify(data, { columns }), output);
11141110
},
11151111
},
@@ -1120,7 +1116,7 @@ Deno.test({
11201116
fn() {
11211117
const columns = [0];
11221118
const data = [[null], [null]];
1123-
const output = `0${NEWLINE}${NEWLINE}${NEWLINE}`;
1119+
const output = `0${CRLF}${CRLF}${CRLF}`;
11241120
assertEquals(stringify(data, { columns }), output);
11251121
},
11261122
},
@@ -1131,7 +1127,7 @@ Deno.test({
11311127
fn() {
11321128
const columns = [0];
11331129
const data = [[0xa], [0xb]];
1134-
const output = `0${NEWLINE}10${NEWLINE}11${NEWLINE}`;
1130+
const output = `0${CRLF}10${CRLF}11${CRLF}`;
11351131
assertEquals(stringify(data, { columns }), output);
11361132
},
11371133
},
@@ -1142,7 +1138,7 @@ Deno.test({
11421138
fn() {
11431139
const columns = [0];
11441140
const data = [[BigInt("1")], [BigInt("2")]];
1145-
const output = `0${NEWLINE}1${NEWLINE}2${NEWLINE}`;
1141+
const output = `0${CRLF}1${CRLF}2${CRLF}`;
11461142
assertEquals(stringify(data, { columns }), output);
11471143
},
11481144
},
@@ -1153,7 +1149,7 @@ Deno.test({
11531149
fn() {
11541150
const columns = [0];
11551151
const data = [[true], [false]];
1156-
const output = `0${NEWLINE}true${NEWLINE}false${NEWLINE}`;
1152+
const output = `0${CRLF}true${CRLF}false${CRLF}`;
11571153
assertEquals(stringify(data, { columns }), output);
11581154
},
11591155
},
@@ -1164,7 +1160,7 @@ Deno.test({
11641160
fn() {
11651161
const columns = [0];
11661162
const data = [["foo"], ["bar"]];
1167-
const output = `0${NEWLINE}foo${NEWLINE}bar${NEWLINE}`;
1163+
const output = `0${CRLF}foo${CRLF}bar${CRLF}`;
11681164
assertEquals(stringify(data, { columns }), output);
11691165
},
11701166
},
@@ -1175,8 +1171,7 @@ Deno.test({
11751171
fn() {
11761172
const columns = [0];
11771173
const data = [[Symbol("foo")], [Symbol("bar")]];
1178-
const output =
1179-
`0${NEWLINE}Symbol(foo)${NEWLINE}Symbol(bar)${NEWLINE}`;
1174+
const output = `0${CRLF}Symbol(foo)${CRLF}Symbol(bar)${CRLF}`;
11801175
assertEquals(stringify(data, { columns }), output);
11811176
},
11821177
},
@@ -1187,7 +1182,7 @@ Deno.test({
11871182
fn() {
11881183
const columns = [0];
11891184
const data = [[(n: number) => n]];
1190-
const output = `0${NEWLINE}(n)=>n${NEWLINE}`;
1185+
const output = `0${CRLF}(n)=>n${CRLF}`;
11911186
assertEquals(stringify(data, { columns }), output);
11921187
},
11931188
},
@@ -1198,7 +1193,7 @@ Deno.test({
11981193
fn() {
11991194
const columns = [0];
12001195
const data = [['foo"']];
1201-
const output = `0${NEWLINE}"foo"""${NEWLINE}`;
1196+
const output = `0${CRLF}"foo"""${CRLF}`;
12021197
assertEquals(stringify(data, { columns }), output);
12031198
},
12041199
},
@@ -1209,7 +1204,7 @@ Deno.test({
12091204
fn() {
12101205
const columns = [0];
12111206
const data = [["foo\r\n"]];
1212-
const output = `0${NEWLINE}"foo\r\n"${NEWLINE}`;
1207+
const output = `0${CRLF}"foo\r\n"${CRLF}`;
12131208
assertEquals(stringify(data, { columns }), output);
12141209
},
12151210
},
@@ -1220,7 +1215,7 @@ Deno.test({
12201215
fn() {
12211216
const columns = [0];
12221217
const data = [["foo\r"]];
1223-
const output = `0${NEWLINE}foo\r${NEWLINE}`;
1218+
const output = `0${CRLF}foo\r${CRLF}`;
12241219
assertEquals(stringify(data, { columns }), output);
12251220
},
12261221
},
@@ -1231,7 +1226,7 @@ Deno.test({
12311226
fn() {
12321227
const columns = [0];
12331228
const data = [["foo\n"]];
1234-
const output = `0${NEWLINE}foo\n${NEWLINE}`;
1229+
const output = `0${CRLF}"foo\n"${CRLF}`;
12351230
assertEquals(stringify(data, { columns }), output);
12361231
},
12371232
},
@@ -1242,7 +1237,7 @@ Deno.test({
12421237
fn() {
12431238
const columns = [0];
12441239
const data = [["foo,"]];
1245-
const output = `0${NEWLINE}"foo,"${NEWLINE}`;
1240+
const output = `0${CRLF}"foo,"${CRLF}`;
12461241
assertEquals(stringify(data, { columns }), output);
12471242
},
12481243
},
@@ -1253,7 +1248,7 @@ Deno.test({
12531248
fn() {
12541249
const columns = [0];
12551250
const data = [["foo,"]];
1256-
const output = `0${NEWLINE}foo,${NEWLINE}`;
1251+
const output = `0${CRLF}foo,${CRLF}`;
12571252

12581253
const options = { separator: "\t", columns };
12591254
assertEquals(stringify(data, options), output);
@@ -1264,7 +1259,7 @@ Deno.test({
12641259
name: "Valid data, no columns",
12651260
async fn() {
12661261
const data = [[1, 2, 3], [4, 5, 6]];
1267-
const output = `${NEWLINE}1,2,3${NEWLINE}4,5,6${NEWLINE}`;
1262+
const output = `${CRLF}1,2,3${CRLF}4,5,6${CRLF}`;
12681263

12691264
assertEquals(await stringify(data), output);
12701265
},

0 commit comments

Comments
 (0)