diff options
| -rw-r--r-- | src/csv_parser.c | 108 | ||||
| -rw-r--r-- | src/csv_parser.h | 10 | ||||
| -rw-r--r-- | tests/main.c | 85 |
3 files changed, 176 insertions, 27 deletions
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; +} diff --git a/src/csv_parser.h b/src/csv_parser.h index 8c78e8b..267c768 100644 --- a/src/csv_parser.h +++ b/src/csv_parser.h @@ -35,6 +35,8 @@ typedef enum csv_parser_state_t { csvps_line_start = 0, csvps_field_start, csvps_field_value, + csvps_field_quoted_value, + csvps_field_quoted_quote, csvps_field_end, csvps_line_end_begin, csvps_line_end, @@ -48,7 +50,7 @@ typedef struct csv_parser_t { int row; /* csv column */ int col; - uint32_t nread; + size_t nread; /* user data */ void *data; } csv_parser_t; @@ -88,6 +90,12 @@ size_t csv_parser_execute(csv_parser_t *parser, const char *data, size_t data_len); +/** + * Flush any pending state. This is useful when the file ends + * without a trailing newline, to flush the final empty field. + * Returns 0 on success, anything else on error. + */ +int csv_parser_finish(csv_parser_t *parser, const csv_parser_settings_t *settings); #ifdef __cplusplus } diff --git a/tests/main.c b/tests/main.c index 90863d8..00a7add 100644 --- a/tests/main.c +++ b/tests/main.c @@ -11,44 +11,83 @@ struct test_results { }; -// 5 items per row, 3 rows per test case: 15 * 6 slots +// 5 items per row, 4 rows per test case: 20 slots static int expected_test_results[] = { - // "" + // 0: "" -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - // ", " + -1, -1, -1, -1, -1, + // 1: ", " 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - // "," - 0, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 2: "," + 0, 0, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - // "\n" + // 3: "\n" -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - // "\r\n", -1, -1, -1, -1, -1, + // 4: "\r\n" -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - // "a,b,,c," - 1, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - // "a,b,,c, " + // 5: "a,bb,,c," + 1, 2, 0, 1, 0, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 6: "a,bb,,c, " 1, 2, 0, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - // "a,b,\na,b," + -1, -1, -1, -1, -1, + // 7: "a,bb,\na,b," 1, 2, 0, -1, -1, - 1, 1, -1, -1, -1, + 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, - // " bc " + -1, -1, -1, -1, -1, + // 8: " bc " 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 9: "a\n\nb" (empty line in middle) + 1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + 1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 10: "\"a\"" (quotes) + 1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 11: "\"a,b\"" (quotes with delimiter) + 3, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 12: "\"a\nb\"" (quotes with newline) + 3, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 13: "\"a\"\"b\"" (escaped quotes) + 3, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + // 14: "\"\"" (empty quotes) + 0, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, }; @@ -66,6 +105,12 @@ static const char *test_data[] = { "a,bb,,c, ", "a,bb,\na,b,", " bc ", + "a\n\nb", + "\"a\"", + "\"a,b\"", + "\"a\nb\"", + "\"a\"\"b\"", + "\"\"" }; @@ -73,12 +118,18 @@ static int test_data_end_states[] = { csvps_line_start, csvps_field_value, csvps_field_start, - csvps_line_end, - csvps_line_end, + csvps_line_start, + csvps_line_start, csvps_field_start, csvps_field_value, csvps_field_start, csvps_field_value, + csvps_field_value, + csvps_field_quoted_quote, + csvps_field_quoted_quote, + csvps_field_quoted_quote, + csvps_field_quoted_quote, + csvps_field_quoted_quote, }; static int num_test_cases = sizeof(test_data)/(sizeof(char*)); @@ -110,7 +161,7 @@ void print_results(int *data, int len) int field_cb(csv_parser_t *parser, const char *data, size_t length, int row, int col) { struct test_results *results = parser->data; - int slot = results->test_case_number * 15 + row * 5 + col; + int slot = results->test_case_number * 20 + row * 5 + col; if (results->results[slot] == -1) { results->results[slot] = length; @@ -182,6 +233,7 @@ START_TEST(test_parser_full) int nread = csv_parser_execute(&parser, &settings, str, len); ck_assert_int_eq(len, nread); ck_assert_int_eq(parser.state, test_data_end_states[i]); + csv_parser_finish(&parser, &settings); } printf("Test results:\n"); @@ -221,6 +273,7 @@ START_TEST(test_parser_chunked) ck_assert_int_eq(1, nread); } ck_assert_int_eq(parser.state, test_data_end_states[i]); + csv_parser_finish(&parser, &settings); } printf("Test results:\n"); |
