From 12007e197832263b9113554bdfea1a8258f179bc Mon Sep 17 00:00:00 2001 From: Tadas Vilkeliskis Date: Thu, 11 Jun 2026 10:00:42 -0400 Subject: Fix multiple parsing bugs and add RFC 4180 quote support - Change nread to size_t to prevent overflow. - Fix state machine to handle consecutive newlines appropriately. - Add csv_parser_finish to emit pending trailing fields at EOF. - Fix nread boundary count on callback errors. - Add support for quoted fields, including escaped quotes. - Update and expand test suite to cover new cases and quote parsing. --- src/csv_parser.c | 108 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 98 insertions(+), 10 deletions(-) (limited to 'src/csv_parser.c') diff --git a/src/csv_parser.c b/src/csv_parser.c index b93713a..32cf8d4 100644 --- a/src/csv_parser.c +++ b/src/csv_parser.c @@ -70,8 +70,15 @@ size_t csv_parser_execute(csv_parser_t *parser, break; case csvps_field_start: parser->col += 1; - parser->state = csvps_field_value; - field_value = cursor; + if (ch == '"') { + parser->state = csvps_field_quoted_value; + cursor++; + parser->nread++; + field_value = cursor; + } else { + parser->state = csvps_field_value; + field_value = cursor; + } break; case csvps_field_value: if (ch == settings->delimiter) { @@ -79,10 +86,6 @@ size_t csv_parser_execute(csv_parser_t *parser, } else if (ch == '\r' || ch == '\n') { parser->state = csvps_line_end_begin; } else { - // If we previously been in csvps_field_value state - // and field_value is not set, set it right away. - // This can happen when we execute parser multiple - // times in the value state. if (field_value == NULL) { field_value = cursor; } @@ -97,6 +100,7 @@ size_t csv_parser_execute(csv_parser_t *parser, parser->col); if (r) { parser->state = csvps_error; + parser->nread++; return parser->nread; } } @@ -104,6 +108,67 @@ size_t csv_parser_execute(csv_parser_t *parser, parser->nread++; } break; + case csvps_field_quoted_value: + if (field_value == NULL) { + field_value = cursor; + } + if (ch == '"') { + parser->state = csvps_field_quoted_quote; + if (settings->field_cb && field_value && (cursor >= field_value)) { + r = settings->field_cb(parser, + field_value, + cursor - field_value, + parser->row, + parser->col); + if (r) { + parser->state = csvps_error; + return parser->nread; + } + } + field_value = NULL; + cursor++; + parser->nread++; + } else { + cursor++; + if (cursor == data_end) { + if (settings->field_cb && field_value) { + r = settings->field_cb(parser, + field_value, + cursor - field_value, + parser->row, + parser->col); + if (r) { + parser->state = csvps_error; + parser->nread++; + return parser->nread; + } + } + } + parser->nread++; + } + break; + case csvps_field_quoted_quote: + if (ch == '"') { + if (settings->field_cb) { + r = settings->field_cb(parser, cursor, 1, parser->row, parser->col); + if (r) { + parser->state = csvps_error; + return parser->nread; + } + } + parser->state = csvps_field_quoted_value; + cursor++; + parser->nread++; + field_value = cursor; + } else if (ch == settings->delimiter) { + parser->state = csvps_field_end; + } else if (ch == '\r' || ch == '\n') { + parser->state = csvps_line_end_begin; + } else { + parser->state = csvps_error; + return parser->nread; + } + break; case csvps_field_end: // callback if (settings->field_cb && field_value) { @@ -135,15 +200,20 @@ size_t csv_parser_execute(csv_parser_t *parser, return parser->nread; } } - parser->state = csvps_line_end; + if (ch == '\r') { + parser->state = csvps_line_end; + } else { + parser->state = csvps_line_start; + } + cursor++; + parser->nread++; break; case csvps_line_end: - if (ch == '\r' || ch == '\n') { + if (ch == '\n') { cursor++; parser->nread++; - } else { - parser->state = csvps_line_start; } + parser->state = csvps_line_start; break; case csvps_error: return parser->nread; @@ -156,3 +226,21 @@ size_t csv_parser_execute(csv_parser_t *parser, return parser->nread; } + +int csv_parser_finish(csv_parser_t *parser, const csv_parser_settings_t *settings) +{ + int r = 0; + if (parser->state == csvps_field_start) { + if (settings->field_cb) { + parser->col += 1; + r = settings->field_cb(parser, "", 0, parser->row, parser->col); + if (r) { + parser->state = csvps_error; + } + } + } + if (parser->state != csvps_error) { + parser->state = csvps_line_start; + } + return r; +} -- cgit v1.2.3