From 37d2274fa03f06b0a6778ef85d8c640f9db8699b Mon Sep 17 00:00:00 2001 From: Agentrix <73670526+pauloflp@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:28:57 -0300 Subject: [PATCH] fix(csv-parse): consume full trim characters after closing quotes --- packages/csv-parse/lib/api/index.js | 4 +- packages/csv-parse/test/option.rtrim.ts | 62 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/csv-parse/lib/api/index.js b/packages/csv-parse/lib/api/index.js index ca97838cd..e9956d055 100644 --- a/packages/csv-parse/lib/api/index.js +++ b/packages/csv-parse/lib/api/index.js @@ -455,9 +455,7 @@ const transform = function (original_options = {}) { ), ); } else { - if (lappend === false) { - pos += this.__isCharTrimable(buf, pos) - 1; - } + pos += this.__isCharTrimable(buf, pos) - 1; continue; } } diff --git a/packages/csv-parse/test/option.rtrim.ts b/packages/csv-parse/test/option.rtrim.ts index c93809126..01ff05c67 100644 --- a/packages/csv-parse/test/option.rtrim.ts +++ b/packages/csv-parse/test/option.rtrim.ts @@ -1,9 +1,71 @@ import "should"; import dedent from "dedent"; import { parse } from "../lib/index.js"; +import { parse as parseSync } from "../lib/sync.js"; import { assert_error } from "./api.assert_error.js"; describe("Option `rtrim`", function () { + for (const encoding of ["utf8", "utf16le"] as const) { + for (const trimming of [{ rtrim: true }, { trim: true }]) { + describe(`${encoding} with ${Object.keys(trimming)[0]}`, function () { + const whitespaces = [" ", "\t", "\u00a0", "\u2003", "\u3000"]; + + it("trims after quoted fields before delimiters and end of input", function () { + for (const whitespace of whitespaces) { + const data = Buffer.from( + `" a "${whitespace},""${whitespace}|"b"${whitespace},"c"${whitespace}`, + encoding, + ); + parseSync(data, { + ...trimming, + encoding, + record_delimiter: "|", + }).should.eql([ + [" a ", ""], + ["b", "c"], + ]); + } + }); + + it("trims after quoted fields with one-byte stream writes", async function () { + for (const whitespace of whitespaces) { + const data = Buffer.from( + `" a "${whitespace},""${whitespace}|"b"${whitespace},"c"${whitespace}`, + encoding, + ); + const records = await new Promise((resolve, reject) => { + const parser = parse( + { ...trimming, encoding, record_delimiter: "|" }, + (err, records) => (err ? reject(err) : resolve(records)), + ); + for (const byte of data) { + parser.write(Buffer.from([byte])); + } + parser.end(); + }); + records.should.eql([ + [" a ", ""], + ["b", "c"], + ]); + } + }); + + it("rejects non-whitespace after trailing whitespace", function () { + for (const whitespace of whitespaces) { + (() => { + parseSync(Buffer.from(`"a"${whitespace}x`, encoding), { + ...trimming, + encoding, + }); + }).should.throw({ + code: "CSV_NON_TRIMABLE_CHAR_AFTER_CLOSING_QUOTE", + }); + } + }); + }); + } + } + it("validation", function () { parse("", { rtrim: true }, () => {}); parse("", { rtrim: false }, () => {});