From 85fe3b67a67825dfc5e43959f001b2a7159dc23f Mon Sep 17 00:00:00 2001 From: Thomas Schmucker Date: Sun, 13 Apr 2025 10:45:35 +0200 Subject: new directory structure --- csv-test.c | 627 --------------------------------------------------- csv.c | 464 -------------------------------------- csv.h | 71 ------ lib/include/csv.h | 71 ++++++ lib/makefile | 0 lib/src/csv.c | 464 ++++++++++++++++++++++++++++++++++++++ tests/makefile | 0 tests/src/csv-test.c | 627 +++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1162 insertions(+), 1162 deletions(-) delete mode 100644 csv-test.c delete mode 100644 csv.c delete mode 100644 csv.h create mode 100644 lib/include/csv.h create mode 100644 lib/makefile create mode 100644 lib/src/csv.c create mode 100644 tests/makefile create mode 100644 tests/src/csv-test.c diff --git a/csv-test.c b/csv-test.c deleted file mode 100644 index c99c049..0000000 --- a/csv-test.c +++ /dev/null @@ -1,627 +0,0 @@ -#undef NDEBUG - -#include -#include -#include -#include -#include - -#include "csv.h" - -#define FMEMOPEN(f, data) \ - f = fmemopen(data, sizeof(data) - 1, "r"); \ - assert(f != NULL) - -#define FMEMCLOSE(f) \ - (void) fclose(f); \ - f = NULL - -#define UNUSED(x) (void) (x) - -void -test_empty_object(void) -{ - { - csv_t csv = { 0 }; - assert(csv_nfields(&csv) == 0); - } - - { - csv_t csv; - csv_init(&csv); - assert(csv_nfields(&csv) == 0); - } -} - -void -test_line_endings1(void) -{ - FILE *file = NULL; - csv_t csv = { 0 }; - - char data[] = "A,B,C\r\n" - "D,E,F\n"; - - FMEMOPEN(file, data); - - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "D") == 0); - assert(strcmp(csv_field(&csv, 1), "E") == 0); - assert(strcmp(csv_field(&csv, 2), "F") == 0); - - assert(csv_read(&csv, file) == 0); - - FMEMCLOSE(file); -} - -void -test_line_endings2(void) -{ - FILE *file = NULL; - csv_t csv = { 0 }; - - char data[] = "A,B,C\r\n" - "D,E,F"; - - FMEMOPEN(file, data); - - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "D") == 0); - assert(strcmp(csv_field(&csv, 1), "E") == 0); - assert(strcmp(csv_field(&csv, 2), "F") == 0); - - assert(csv_read(&csv, file) == 0); - - FMEMCLOSE(file); -} - -void -test_empty_fields(void) -{ - FILE *file = NULL; - csv_t csv = { 0 }; - - char data[] = "\n" - "\r" - "\r\n" - "\"\"\n" - "\"\"\r" - "\"\"\r\n" - ",\n" - ",\r" - ",\r\n" - "\"\",\"\"\n" - "\"\",\"\"\r" - "\"\",\"\"\r\n" - ",,\n" - ",,\r" - ",,\r\n" - "\"\",\"\",\n" - "\"\",\"\",\r" - "\"\",\"\",\r\n" - ",,"; - - FMEMOPEN(file, data); - - // Line 1 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "") == 0); - - // Line 2 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "") == 0); - - // Line 3 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "") == 0); - - // Line 4 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "") == 0); - - // Line 5 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "") == 0); - - // Line 6 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "") == 0); - - // Line 7 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - - // Line 8 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - - // Line 9 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - - // Line 10 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - - // Line 11 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - - // Line 12 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - - // Line 13 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - assert(strcmp(csv_field(&csv, 2), "") == 0); - - // Line 14 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - assert(strcmp(csv_field(&csv, 2), "") == 0); - - // Line 15 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - assert(strcmp(csv_field(&csv, 2), "") == 0); - - // Line 16 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - assert(strcmp(csv_field(&csv, 2), "") == 0); - - // Line 17 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - assert(strcmp(csv_field(&csv, 2), "") == 0); - - // Line 18 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - assert(strcmp(csv_field(&csv, 2), "") == 0); - - // Line 19 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "") == 0); - assert(strcmp(csv_field(&csv, 1), "") == 0); - assert(strcmp(csv_field(&csv, 2), "") == 0); - - // EOF - assert(csv_read(&csv, file) == 0); - - FMEMCLOSE(file); -} - -void -test_simple_fields(void) -{ - FILE *file = NULL; - csv_t csv = { 0 }; - - char data[] = "A\n" - "A\r" - "A\r\n" - "\"A\"\n" - "\"A\"\r" - "\"A\"\r\n" - "A,B\n" - "A,B\r" - "A,B\r\n" - "\"A\",\"B\"\n" - "\"A\",\"B\"\r" - "\"A\",\"B\"\r\n" - "A,B,C\n" - "A,B,C\r" - "A,B,C\r\n" - "\"A\",\"B\",C\n" - "\"A\",\"B\",C\r" - "\"A\",\"B\",C\r\n" - "A,B,C"; - - FMEMOPEN(file, data); - - // Line 1 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - - // Line 2 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - - // Line 3 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - - // Line 4 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - - // Line 5 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - - // Line 6 - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - - // Line 7 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - - // Line 8 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - - // Line 9 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - - // Line 10 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - - // Line 11 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - - // Line 12 - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - - // Line 13 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - // Line 14 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - // Line 15 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - // Line 16 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - // Line 17 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - // Line 18 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - // Line 19 - assert(csv_read(&csv, file) == 3); - assert(csv_nfields(&csv) == 3); - assert(strcmp(csv_field(&csv, 0), "A") == 0); - assert(strcmp(csv_field(&csv, 1), "B") == 0); - assert(strcmp(csv_field(&csv, 2), "C") == 0); - - // EOF - assert(csv_read(&csv, file) == 0); - - FMEMCLOSE(file); -} - -void -test_quoted_fields(void) -{ - FILE *file = NULL; - csv_t csv = { 0 }; - - char data[] = "foo \"baz\" bar,foo \"\"baz\"\" bar,\"foo \"\"baz\"\" bar\",\"foo \"baz\" bar\",\"foo \"\"baz\"\", bar\""; - - FMEMOPEN(file, data); - - assert(csv_read(&csv, file) == 5); - assert(csv_nfields(&csv) == 5); - assert(strcmp(csv_field(&csv, 0), "foo \"baz\" bar") == 0); - assert(strcmp(csv_field(&csv, 1), "foo \"\"baz\"\" bar") == 0); - assert(strcmp(csv_field(&csv, 2), "foo \"baz\" bar") == 0); - assert(strcmp(csv_field(&csv, 3), "foo \"baz\" bar") == 0); - assert(strcmp(csv_field(&csv, 4), "foo \"baz\", bar") == 0); - - assert(csv_read(&csv, file) == 0); - - FMEMCLOSE(file); -} - -void -test_wrong_quoted_field(void) -{ - FILE *file = NULL; - csv_t csv = { 0 }; - - char data[] = "\"foo"; - - FMEMOPEN(file, data); - - assert(csv_read(&csv, file) == 1); - assert(csv_nfields(&csv) == 1); - assert(strcmp(csv_field(&csv, 0), "foo") == 0); - - assert(csv_read(&csv, file) == 0); - - FMEMCLOSE(file); -} - -static void -cb_error(csv_err_t err, void *cb_arg) -{ - longjmp(*((jmp_buf *) cb_arg), err); -} - -void -test_out_of_range_error(void) -{ - jmp_buf env; - csv_options_t csv_options = csv_default_options; - csv_options.cb_error = cb_error; - csv_options.cb_error_arg = &env; - - csv_t csv; - csv_init_opt(&csv, &csv_options); - - FILE *file = NULL; - char data[] = ","; - - FMEMOPEN(file, data); - - switch ( setjmp(env) ) { - case CSV_ERR_OK: - assert(csv_read(&csv, file) == 2); - assert(csv_nfields(&csv) == 2); - - // force an "out of range"-Exception! - assert(strcmp(csv_field(&csv, 2), "will fail") == 0); - break; - - case CSV_ERR_OUT_OF_RANGE: - csv_cleanup(&csv); - break; - - default: - assert(!"this should not be happen!"); - } - - FMEMCLOSE(file); -} - -void -test_read_error(void) -{ - jmp_buf env; - csv_options_t csv_options = csv_default_options; - csv_options.cb_error = cb_error; - csv_options.cb_error_arg = &env; - - csv_t csv; - csv_init_opt(&csv, &csv_options); - - FILE *file = fopen("/dev/null", "r"); - assert(file != NULL); - (void) fputc('T', file); // Set Error-Flag! - - switch ( setjmp(env) ) { - case CSV_ERR_OK: - assert(csv_read(&csv, file) == 0); - break; - - case CSV_ERR_IO_READ: - csv_cleanup(&csv); - break; - - default: - assert(!"this should not be happen!"); - } - - (void) fclose(file); -} - -void * -my_allocate(size_t n, size_t size, void *cb_arg) -{ - UNUSED(cb_arg); - - void *ptr = calloc(n, size); - - return ptr; -} - -void * -my_allocate_null(size_t n, size_t size, void *cb_arg) -{ - UNUSED(n); - UNUSED(size); - UNUSED(cb_arg); - - return NULL; -} - -void * -my_reallocate(void *ptr, size_t n, size_t size, void *cb_arg) -{ - UNUSED(ptr); - UNUSED(n); - UNUSED(size); - UNUSED(cb_arg); - - return NULL; -} - -void -my_free(void *ptr, size_t n, size_t size, void *cb_arg) -{ - UNUSED(n); - UNUSED(size); - UNUSED(cb_arg); - - free(ptr); -} - -void -test_allocation_error1(void) -{ - jmp_buf env; - - csv_options_t csv_options = csv_default_options; - csv_options.cb_error = cb_error; - csv_options.cb_error_arg = &env; - csv_options.cb_allocate = my_allocate; - csv_options.cb_reallocate = my_reallocate; - csv_options.cb_free = my_free; - - csv_t csv; - csv_init_opt(&csv, &csv_options); - - char data[] = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T"; - - FILE *file = NULL; - FMEMOPEN(file, data); - assert(file != NULL); - - switch ( setjmp(env) ) { - case CSV_ERR_OK: - assert(csv_read(&csv, file) == 20); - assert(!"this should not be happen!"); - break; - - case CSV_ERR_OUT_OF_MEMORY: - csv_cleanup(&csv); - break; - - default: - assert(!"this should not be happen!"); - } - - FMEMCLOSE(file); -} - -void -show_version(void) -{ - const char *version = csv_version; - - printf("libcsv - version: "); - for ( ; *version; ++version ) { - putchar(*version); - } - - printf(", build date: "); - for ( ++version; *version; ++version ) { - putchar(*version); - } - - printf(", build time: "); - for ( ++version; *version; ++version ) { - putchar(*version); - } - putchar('\n'); -} - -int -main(void) -{ - show_version(); - - test_empty_object(); - test_line_endings1(); - test_line_endings2(); - test_empty_fields(); - test_simple_fields(); - test_quoted_fields(); - test_wrong_quoted_field(); - - test_out_of_range_error(); - test_read_error(); - - test_allocation_error1(); - - puts("all tests passed..."); - - return EXIT_SUCCESS; -} diff --git a/csv.c b/csv.c deleted file mode 100644 index e9c7464..0000000 --- a/csv.c +++ /dev/null @@ -1,464 +0,0 @@ -#include "csv.h" - -#include -#include -#include -#include -#include - -// === Compile time options === - -#ifndef CSV_DEFAULT_DELIMITER -# define CSV_DEFAULT_DELIMITER '"' -#endif - -#ifndef CSV_DEFAULT_SEPARATOR -# define CSV_DEFAULT_SEPARATOR ',' -#endif - -// === some useful Makros === - -#define UNUSED(x) (void) (x) // mark a parameter as 'unused' -#define STR(s) #s // Stringify a Makro -#define XSTR(s) STR(s) - -// === Semantic Version Information === - -#define CSV_VER_MAJOR 1 -#define CSV_VER_MINOR 0 -#define CSV_VER_PATCH 0 -#define CSV_VER_APPENDIX "-dev" - -const char csv_version[] = XSTR(CSV_VER_MAJOR) "." XSTR(CSV_VER_MINOR) "." XSTR(CSV_VER_PATCH) CSV_VER_APPENDIX "\0" __DATE__ "\0" __TIME__; - -// === CSV-MEMORY Interface === - -static void * -csv_mem_reallocate(void *ptr, size_t num, size_t size, void *cb_arg) -{ - UNUSED(cb_arg); - - return reallocarray(ptr, num, size); -} - -static void -csv_mem_free(void *ptr, size_t num, size_t size, void *cb_arg) -{ - UNUSED(num); - UNUSED(size); - UNUSED(cb_arg); - - free(ptr); -} - -// === CSV-OPTIONS Interface === - -const csv_options_t csv_default_options = { - .field_delimiter = CSV_DEFAULT_DELIMITER, - .field_separator = CSV_DEFAULT_SEPARATOR, - .cb_error = NULL, - .cb_reallocate = csv_mem_reallocate, - .cb_free = csv_mem_free -}; - -// === CSV-ERROR Interface === - -static void -csv_fatal_error(csv_err_t csv_err, const csv_options_t *const csv_options) -{ - if ( csv_options->cb_error != NULL ) { - (*csv_options->cb_error)(csv_err, csv_options->cb_error_arg); - } - (void) fprintf(stderr, "fatal error: %s\n", csv_err_str(csv_err)); - abort(); -} - -// === CSV-STRING Interface === - -static inline void -csv_string_init(csv_string_t *csv_string) -{ - assert(csv_string != NULL); - - csv_string->str = NULL; - csv_string->cap = 0; - csv_string->pos = 0; -} - -static inline void -csv_string_reset(csv_string_t *csv_string) -{ - assert(csv_string != NULL); - - csv_string->pos = 0; -} - -static inline int -csv_string_isempty(csv_string_t *csv_string) -{ - assert(csv_string != NULL); - - return (csv_string->pos == 0) ? 1 : 0; -} - -static inline size_t -growth_strategy(size_t current_cap) -{ - static const size_t INITIAL_CAP = 16; - - return (current_cap == 0) ? INITIAL_CAP : (current_cap * 3) / 2; -} - -static inline void -csv_string_grow_if_needed(csv_string_t *csv_string, const csv_options_t *const csv_options) -{ - assert(csv_string != NULL); - assert(csv_options != NULL); - - if ( csv_string->pos == csv_string->cap ) { - size_t cap = growth_strategy(csv_string->cap); - char *str = csv_options->cb_reallocate(csv_string->str, cap, 1, csv_options->cb_memory_arg); - if ( str == NULL ) { - csv_fatal_error(CSV_ERR_OUT_OF_MEMORY, csv_options); - return; - } - csv_string->str = str; - csv_string->cap = cap; - } -} - -static inline void -csv_string_append(csv_string_t *csv_string, int ch, const csv_options_t *const csv_options) -{ - assert(csv_string != NULL); - assert(csv_options != NULL); - - csv_string_grow_if_needed(csv_string, csv_options); - - csv_string->str[csv_string->pos++] = (char) ch; // append char -} - -static inline void -csv_string_free(csv_string_t *csv_string, const csv_options_t *const csv_options) -{ - assert(csv_string != NULL); - assert(csv_options != NULL); - - csv_options->cb_free(csv_string->str, csv_string->cap, 1, csv_options->cb_memory_arg); - - // call *_init() for sane default values; prevent possible double-free - csv_string_init(csv_string); -} - -// === CSV-FIELD Interface === - -static inline void -csv_field_init(csv_field_t *csv_field) -{ - assert(csv_field != NULL); - - csv_field->fields = NULL; - csv_field->cap = 0; - csv_field->pos = 0; -} - -static inline void -csv_field_reset(csv_field_t *csv_field) -{ - assert(csv_field != NULL); - - csv_field->pos = 0; -} - -static inline void -csv_field_grow_if_needed(csv_field_t *csv_field, const csv_options_t *const csv_options) -{ - assert(csv_field != NULL); - assert(csv_options != NULL); - - if ( csv_field->pos == csv_field->cap ) { - size_t cap = growth_strategy(csv_field->cap); - size_t *fields = csv_options->cb_reallocate(csv_field->fields, cap, sizeof(csv_field->fields[0]), csv_options->cb_memory_arg); - if ( fields == NULL ) { - csv_fatal_error(CSV_ERR_OUT_OF_MEMORY, csv_options); - return; - } - csv_field->fields = fields; - csv_field->cap = cap; - } -} - -static inline void -csv_field_append(csv_field_t *csv_field, size_t idx, const csv_options_t *const csv_options) -{ - assert(csv_field != NULL); - assert(csv_options != NULL); - - csv_field_grow_if_needed(csv_field, csv_options); - - csv_field->fields[csv_field->pos++] = idx; // append index -} - -static inline void -csv_field_free(csv_field_t *csv_field, const csv_options_t *const csv_options) -{ - assert(csv_field != NULL); - assert(csv_options != NULL); - - csv_options->cb_free(csv_field->fields, csv_field->cap, sizeof(csv_field->fields[0]), csv_options->cb_memory_arg); - - csv_field_init(csv_field); -} - -// === CSV Interface === - -void -csv_init(csv_t *csv) -{ - assert(csv != NULL); - - csv_init_opt(csv, NULL); -} - -void -csv_init_opt(csv_t *csv, const csv_options_t *const csv_options) -{ - assert(csv != NULL); - - if ( csv_options != NULL ) { - csv->csv_options = csv_options; - } - else { - csv->csv_options = &csv_default_options; - } - - csv_string_init(&csv->csv_string); - csv_field_init(&csv->csv_field); -} - -void -csv_cleanup(csv_t *csv) -{ - assert(csv != NULL); - assert(csv->csv_options != NULL); - - csv_string_free(&csv->csv_string, csv->csv_options); - csv_field_free(&csv->csv_field, csv->csv_options); -} - -size_t -csv_nfields(const csv_t *const csv) -{ - assert(csv != NULL); - - return csv->csv_field.pos; -} - -const char * -csv_field(const csv_t *const csv, size_t idx) -{ - assert(csv != NULL); - assert(idx >= 0 && idx < csv->csv_field.pos); - - if ( idx >= csv->csv_field.pos ) { - csv_fatal_error(CSV_ERR_OUT_OF_RANGE, csv->csv_options); - return NULL; - } - return &csv->csv_string.str[csv->csv_field.fields[idx]]; -} - -size_t -csv_read(csv_t *csv, FILE *in) -{ - assert(csv != NULL); - assert(in != NULL); - - // initialize if needed... - if ( csv->csv_options == NULL ) { - csv_init(csv); - } - - if ( ferror(in) ) { - csv_fatal_error(CSV_ERR_IO_READ, csv->csv_options); - return 0; - } - - // do not try to read if EOF has already been seen - if ( feof(in) ) { - csv_cleanup(csv); - return 0; - } - - enum { - STATE_START_FIELD, - STATE_QUOTED_FIELD, - STATE_SIMPLE_FIELD, - STATE_END_FIELD, - STATE_END_LINE, - STATE_END_FILE, - }; - - register const int DELIM = csv->csv_options->field_delimiter; - register const int SEP = csv->csv_options->field_separator; - - csv_string_reset(&csv->csv_string); - csv_field_reset(&csv->csv_field); - - for ( int state = STATE_START_FIELD;; ) { - int ch; - - switch ( state ) { - case STATE_START_FIELD: - csv_field_append(&csv->csv_field, csv->csv_string.pos, csv->csv_options); - - ch = getc(in); - if ( ch == EOF ) { - state = STATE_END_FILE; - } - else if ( ch == '\r' ) { // test for CR.. - ch = getc(in); - if ( ch != '\n' ) { // ..LF - (void) ungetc(ch, in); - } - state = STATE_END_LINE; - } - else if ( ch == '\n' ) { - state = STATE_END_LINE; - } - else if ( ch == SEP ) { - state = STATE_END_FIELD; - } - else if ( ch == DELIM ) { - state = STATE_QUOTED_FIELD; - } - else { - if ( ch != '\0' ) { - csv_string_append(&csv->csv_string, ch, csv->csv_options); - } - state = STATE_SIMPLE_FIELD; - } - break; - - case STATE_QUOTED_FIELD: - do { - ch = getc(in); - if ( ch == EOF ) { - state = STATE_END_FILE; - } - else if ( ch == DELIM ) { - ch = getc(in); - if ( ch == EOF ) { - state = STATE_END_FILE; - } - else if ( ch == DELIM ) { - csv_string_append(&csv->csv_string, DELIM, csv->csv_options); - } - else if ( ch == SEP ) { - state = STATE_END_FIELD; - } - else if ( ch == '\r' ) { - ch = getc(in); - if ( ch != '\n' ) { - (void) ungetc(ch, in); - } - state = STATE_END_LINE; - } - else if ( ch == '\n' ) { - state = STATE_END_LINE; - } - else { - csv_string_append(&csv->csv_string, DELIM, csv->csv_options); - (void) ungetc(ch, in); // we have read too far... Put the character back! - } - } - else { - if ( ch != '\0' ) { - csv_string_append(&csv->csv_string, ch, csv->csv_options); - } - } - } while ( state == STATE_QUOTED_FIELD ); - break; - - case STATE_SIMPLE_FIELD: - do { - ch = getc(in); - if ( ch == EOF ) { - state = STATE_END_FILE; - } - else if ( ch == SEP ) { - state = STATE_END_FIELD; - } - else if ( ch == '\r' ) { - ch = getc(in); - if ( ch != '\n' ) { - (void) ungetc(ch, in); - } - state = STATE_END_LINE; - } - else if ( ch == '\n' ) { - state = STATE_END_LINE; - } - else { - if ( ch != '\0' ) { - csv_string_append(&csv->csv_string, ch, csv->csv_options); - } - } - } while ( state == STATE_SIMPLE_FIELD ); - break; - - case STATE_END_FIELD: - csv_string_append(&csv->csv_string, '\0', csv->csv_options); - state = STATE_START_FIELD; - break; - - case STATE_END_LINE: - csv_string_append(&csv->csv_string, '\0', csv->csv_options); - return csv->csv_field.pos; - - case STATE_END_FILE: - if ( ferror(in) ) { - csv_fatal_error(CSV_ERR_IO_READ, csv->csv_options); - return 0; - } - - if ( csv_string_isempty(&csv->csv_string) ) { - csv_cleanup(csv); - return 0; // EOF reached - } - - /* - * The last data record was not terminated with a NEWLINE-Symbol. - * So we can't signal End-Of-File for now. Terminate the current - * field and return the number of fields processed so far. - */ - csv_string_append(&csv->csv_string, '\0', csv->csv_options); - - return csv->csv_field.pos; - - default: - assert(!"this should never be happen..."); - break; - } - } - // NOT REACHED -} - -const char * -csv_err_str(csv_err_t csv_err) -{ - switch ( csv_err ) { - case CSV_ERR_OK: - return "no error"; - case CSV_ERR_OUT_OF_MEMORY: - return "out of memory"; - case CSV_ERR_OUT_OF_RANGE: - return "index out of range"; - case CSV_ERR_IO_READ: - return "read error"; - case CSV_ERR_IO_WRITE: - return "write error"; - default: - return "unknown error"; - } - // NOT REACHED -} diff --git a/csv.h b/csv.h deleted file mode 100644 index 9ac7c05..0000000 --- a/csv.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include /* size_t */ -#include /* FILE */ - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum { - CSV_ERR_OK = 0, - CSV_ERR_OUT_OF_MEMORY = -1, - CSV_ERR_OUT_OF_RANGE = -2, - CSV_ERR_IO_READ = -3, - CSV_ERR_IO_WRITE = -4 -} csv_err_t; - -typedef struct { - int field_delimiter; - int field_separator; - - void (*cb_error)(csv_err_t, void *); - void *cb_error_arg; - - void *(*cb_allocate)(size_t, size_t, void *); - void *(*cb_reallocate)(void *, size_t, size_t, void *); - void (*cb_free)(void *, size_t, size_t, void *); - void *cb_memory_arg; -} csv_options_t; - -extern const csv_options_t csv_default_options; - -typedef struct { - char * str; - size_t cap, pos; -} csv_string_t; - -typedef struct { - size_t *fields; - size_t cap, pos; -} csv_field_t; - -typedef struct { - const csv_options_t *csv_options; - csv_string_t csv_string; - csv_field_t csv_field; -} csv_t; - -/* version */ -extern const char csv_version[]; - -/* initialization */ -void csv_init(csv_t *csv); -void csv_init_opt(csv_t *csv, const csv_options_t *const csv_options); - -/* cleanup after error */ -void csv_cleanup(csv_t *csv); - -/* read */ -size_t csv_read(csv_t *csv, FILE *in); - -/* field access */ -size_t csv_nfields(const csv_t *const csv); -const char *csv_field(const csv_t *const csv, size_t idx); - -/* error handling */ -const char *csv_err_str(csv_err_t csv_err); - -#ifdef __cplusplus -} -#endif diff --git a/lib/include/csv.h b/lib/include/csv.h new file mode 100644 index 0000000..9ac7c05 --- /dev/null +++ b/lib/include/csv.h @@ -0,0 +1,71 @@ +#pragma once + +#include /* size_t */ +#include /* FILE */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + CSV_ERR_OK = 0, + CSV_ERR_OUT_OF_MEMORY = -1, + CSV_ERR_OUT_OF_RANGE = -2, + CSV_ERR_IO_READ = -3, + CSV_ERR_IO_WRITE = -4 +} csv_err_t; + +typedef struct { + int field_delimiter; + int field_separator; + + void (*cb_error)(csv_err_t, void *); + void *cb_error_arg; + + void *(*cb_allocate)(size_t, size_t, void *); + void *(*cb_reallocate)(void *, size_t, size_t, void *); + void (*cb_free)(void *, size_t, size_t, void *); + void *cb_memory_arg; +} csv_options_t; + +extern const csv_options_t csv_default_options; + +typedef struct { + char * str; + size_t cap, pos; +} csv_string_t; + +typedef struct { + size_t *fields; + size_t cap, pos; +} csv_field_t; + +typedef struct { + const csv_options_t *csv_options; + csv_string_t csv_string; + csv_field_t csv_field; +} csv_t; + +/* version */ +extern const char csv_version[]; + +/* initialization */ +void csv_init(csv_t *csv); +void csv_init_opt(csv_t *csv, const csv_options_t *const csv_options); + +/* cleanup after error */ +void csv_cleanup(csv_t *csv); + +/* read */ +size_t csv_read(csv_t *csv, FILE *in); + +/* field access */ +size_t csv_nfields(const csv_t *const csv); +const char *csv_field(const csv_t *const csv, size_t idx); + +/* error handling */ +const char *csv_err_str(csv_err_t csv_err); + +#ifdef __cplusplus +} +#endif diff --git a/lib/makefile b/lib/makefile new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/csv.c b/lib/src/csv.c new file mode 100644 index 0000000..e9c7464 --- /dev/null +++ b/lib/src/csv.c @@ -0,0 +1,464 @@ +#include "csv.h" + +#include +#include +#include +#include +#include + +// === Compile time options === + +#ifndef CSV_DEFAULT_DELIMITER +# define CSV_DEFAULT_DELIMITER '"' +#endif + +#ifndef CSV_DEFAULT_SEPARATOR +# define CSV_DEFAULT_SEPARATOR ',' +#endif + +// === some useful Makros === + +#define UNUSED(x) (void) (x) // mark a parameter as 'unused' +#define STR(s) #s // Stringify a Makro +#define XSTR(s) STR(s) + +// === Semantic Version Information === + +#define CSV_VER_MAJOR 1 +#define CSV_VER_MINOR 0 +#define CSV_VER_PATCH 0 +#define CSV_VER_APPENDIX "-dev" + +const char csv_version[] = XSTR(CSV_VER_MAJOR) "." XSTR(CSV_VER_MINOR) "." XSTR(CSV_VER_PATCH) CSV_VER_APPENDIX "\0" __DATE__ "\0" __TIME__; + +// === CSV-MEMORY Interface === + +static void * +csv_mem_reallocate(void *ptr, size_t num, size_t size, void *cb_arg) +{ + UNUSED(cb_arg); + + return reallocarray(ptr, num, size); +} + +static void +csv_mem_free(void *ptr, size_t num, size_t size, void *cb_arg) +{ + UNUSED(num); + UNUSED(size); + UNUSED(cb_arg); + + free(ptr); +} + +// === CSV-OPTIONS Interface === + +const csv_options_t csv_default_options = { + .field_delimiter = CSV_DEFAULT_DELIMITER, + .field_separator = CSV_DEFAULT_SEPARATOR, + .cb_error = NULL, + .cb_reallocate = csv_mem_reallocate, + .cb_free = csv_mem_free +}; + +// === CSV-ERROR Interface === + +static void +csv_fatal_error(csv_err_t csv_err, const csv_options_t *const csv_options) +{ + if ( csv_options->cb_error != NULL ) { + (*csv_options->cb_error)(csv_err, csv_options->cb_error_arg); + } + (void) fprintf(stderr, "fatal error: %s\n", csv_err_str(csv_err)); + abort(); +} + +// === CSV-STRING Interface === + +static inline void +csv_string_init(csv_string_t *csv_string) +{ + assert(csv_string != NULL); + + csv_string->str = NULL; + csv_string->cap = 0; + csv_string->pos = 0; +} + +static inline void +csv_string_reset(csv_string_t *csv_string) +{ + assert(csv_string != NULL); + + csv_string->pos = 0; +} + +static inline int +csv_string_isempty(csv_string_t *csv_string) +{ + assert(csv_string != NULL); + + return (csv_string->pos == 0) ? 1 : 0; +} + +static inline size_t +growth_strategy(size_t current_cap) +{ + static const size_t INITIAL_CAP = 16; + + return (current_cap == 0) ? INITIAL_CAP : (current_cap * 3) / 2; +} + +static inline void +csv_string_grow_if_needed(csv_string_t *csv_string, const csv_options_t *const csv_options) +{ + assert(csv_string != NULL); + assert(csv_options != NULL); + + if ( csv_string->pos == csv_string->cap ) { + size_t cap = growth_strategy(csv_string->cap); + char *str = csv_options->cb_reallocate(csv_string->str, cap, 1, csv_options->cb_memory_arg); + if ( str == NULL ) { + csv_fatal_error(CSV_ERR_OUT_OF_MEMORY, csv_options); + return; + } + csv_string->str = str; + csv_string->cap = cap; + } +} + +static inline void +csv_string_append(csv_string_t *csv_string, int ch, const csv_options_t *const csv_options) +{ + assert(csv_string != NULL); + assert(csv_options != NULL); + + csv_string_grow_if_needed(csv_string, csv_options); + + csv_string->str[csv_string->pos++] = (char) ch; // append char +} + +static inline void +csv_string_free(csv_string_t *csv_string, const csv_options_t *const csv_options) +{ + assert(csv_string != NULL); + assert(csv_options != NULL); + + csv_options->cb_free(csv_string->str, csv_string->cap, 1, csv_options->cb_memory_arg); + + // call *_init() for sane default values; prevent possible double-free + csv_string_init(csv_string); +} + +// === CSV-FIELD Interface === + +static inline void +csv_field_init(csv_field_t *csv_field) +{ + assert(csv_field != NULL); + + csv_field->fields = NULL; + csv_field->cap = 0; + csv_field->pos = 0; +} + +static inline void +csv_field_reset(csv_field_t *csv_field) +{ + assert(csv_field != NULL); + + csv_field->pos = 0; +} + +static inline void +csv_field_grow_if_needed(csv_field_t *csv_field, const csv_options_t *const csv_options) +{ + assert(csv_field != NULL); + assert(csv_options != NULL); + + if ( csv_field->pos == csv_field->cap ) { + size_t cap = growth_strategy(csv_field->cap); + size_t *fields = csv_options->cb_reallocate(csv_field->fields, cap, sizeof(csv_field->fields[0]), csv_options->cb_memory_arg); + if ( fields == NULL ) { + csv_fatal_error(CSV_ERR_OUT_OF_MEMORY, csv_options); + return; + } + csv_field->fields = fields; + csv_field->cap = cap; + } +} + +static inline void +csv_field_append(csv_field_t *csv_field, size_t idx, const csv_options_t *const csv_options) +{ + assert(csv_field != NULL); + assert(csv_options != NULL); + + csv_field_grow_if_needed(csv_field, csv_options); + + csv_field->fields[csv_field->pos++] = idx; // append index +} + +static inline void +csv_field_free(csv_field_t *csv_field, const csv_options_t *const csv_options) +{ + assert(csv_field != NULL); + assert(csv_options != NULL); + + csv_options->cb_free(csv_field->fields, csv_field->cap, sizeof(csv_field->fields[0]), csv_options->cb_memory_arg); + + csv_field_init(csv_field); +} + +// === CSV Interface === + +void +csv_init(csv_t *csv) +{ + assert(csv != NULL); + + csv_init_opt(csv, NULL); +} + +void +csv_init_opt(csv_t *csv, const csv_options_t *const csv_options) +{ + assert(csv != NULL); + + if ( csv_options != NULL ) { + csv->csv_options = csv_options; + } + else { + csv->csv_options = &csv_default_options; + } + + csv_string_init(&csv->csv_string); + csv_field_init(&csv->csv_field); +} + +void +csv_cleanup(csv_t *csv) +{ + assert(csv != NULL); + assert(csv->csv_options != NULL); + + csv_string_free(&csv->csv_string, csv->csv_options); + csv_field_free(&csv->csv_field, csv->csv_options); +} + +size_t +csv_nfields(const csv_t *const csv) +{ + assert(csv != NULL); + + return csv->csv_field.pos; +} + +const char * +csv_field(const csv_t *const csv, size_t idx) +{ + assert(csv != NULL); + assert(idx >= 0 && idx < csv->csv_field.pos); + + if ( idx >= csv->csv_field.pos ) { + csv_fatal_error(CSV_ERR_OUT_OF_RANGE, csv->csv_options); + return NULL; + } + return &csv->csv_string.str[csv->csv_field.fields[idx]]; +} + +size_t +csv_read(csv_t *csv, FILE *in) +{ + assert(csv != NULL); + assert(in != NULL); + + // initialize if needed... + if ( csv->csv_options == NULL ) { + csv_init(csv); + } + + if ( ferror(in) ) { + csv_fatal_error(CSV_ERR_IO_READ, csv->csv_options); + return 0; + } + + // do not try to read if EOF has already been seen + if ( feof(in) ) { + csv_cleanup(csv); + return 0; + } + + enum { + STATE_START_FIELD, + STATE_QUOTED_FIELD, + STATE_SIMPLE_FIELD, + STATE_END_FIELD, + STATE_END_LINE, + STATE_END_FILE, + }; + + register const int DELIM = csv->csv_options->field_delimiter; + register const int SEP = csv->csv_options->field_separator; + + csv_string_reset(&csv->csv_string); + csv_field_reset(&csv->csv_field); + + for ( int state = STATE_START_FIELD;; ) { + int ch; + + switch ( state ) { + case STATE_START_FIELD: + csv_field_append(&csv->csv_field, csv->csv_string.pos, csv->csv_options); + + ch = getc(in); + if ( ch == EOF ) { + state = STATE_END_FILE; + } + else if ( ch == '\r' ) { // test for CR.. + ch = getc(in); + if ( ch != '\n' ) { // ..LF + (void) ungetc(ch, in); + } + state = STATE_END_LINE; + } + else if ( ch == '\n' ) { + state = STATE_END_LINE; + } + else if ( ch == SEP ) { + state = STATE_END_FIELD; + } + else if ( ch == DELIM ) { + state = STATE_QUOTED_FIELD; + } + else { + if ( ch != '\0' ) { + csv_string_append(&csv->csv_string, ch, csv->csv_options); + } + state = STATE_SIMPLE_FIELD; + } + break; + + case STATE_QUOTED_FIELD: + do { + ch = getc(in); + if ( ch == EOF ) { + state = STATE_END_FILE; + } + else if ( ch == DELIM ) { + ch = getc(in); + if ( ch == EOF ) { + state = STATE_END_FILE; + } + else if ( ch == DELIM ) { + csv_string_append(&csv->csv_string, DELIM, csv->csv_options); + } + else if ( ch == SEP ) { + state = STATE_END_FIELD; + } + else if ( ch == '\r' ) { + ch = getc(in); + if ( ch != '\n' ) { + (void) ungetc(ch, in); + } + state = STATE_END_LINE; + } + else if ( ch == '\n' ) { + state = STATE_END_LINE; + } + else { + csv_string_append(&csv->csv_string, DELIM, csv->csv_options); + (void) ungetc(ch, in); // we have read too far... Put the character back! + } + } + else { + if ( ch != '\0' ) { + csv_string_append(&csv->csv_string, ch, csv->csv_options); + } + } + } while ( state == STATE_QUOTED_FIELD ); + break; + + case STATE_SIMPLE_FIELD: + do { + ch = getc(in); + if ( ch == EOF ) { + state = STATE_END_FILE; + } + else if ( ch == SEP ) { + state = STATE_END_FIELD; + } + else if ( ch == '\r' ) { + ch = getc(in); + if ( ch != '\n' ) { + (void) ungetc(ch, in); + } + state = STATE_END_LINE; + } + else if ( ch == '\n' ) { + state = STATE_END_LINE; + } + else { + if ( ch != '\0' ) { + csv_string_append(&csv->csv_string, ch, csv->csv_options); + } + } + } while ( state == STATE_SIMPLE_FIELD ); + break; + + case STATE_END_FIELD: + csv_string_append(&csv->csv_string, '\0', csv->csv_options); + state = STATE_START_FIELD; + break; + + case STATE_END_LINE: + csv_string_append(&csv->csv_string, '\0', csv->csv_options); + return csv->csv_field.pos; + + case STATE_END_FILE: + if ( ferror(in) ) { + csv_fatal_error(CSV_ERR_IO_READ, csv->csv_options); + return 0; + } + + if ( csv_string_isempty(&csv->csv_string) ) { + csv_cleanup(csv); + return 0; // EOF reached + } + + /* + * The last data record was not terminated with a NEWLINE-Symbol. + * So we can't signal End-Of-File for now. Terminate the current + * field and return the number of fields processed so far. + */ + csv_string_append(&csv->csv_string, '\0', csv->csv_options); + + return csv->csv_field.pos; + + default: + assert(!"this should never be happen..."); + break; + } + } + // NOT REACHED +} + +const char * +csv_err_str(csv_err_t csv_err) +{ + switch ( csv_err ) { + case CSV_ERR_OK: + return "no error"; + case CSV_ERR_OUT_OF_MEMORY: + return "out of memory"; + case CSV_ERR_OUT_OF_RANGE: + return "index out of range"; + case CSV_ERR_IO_READ: + return "read error"; + case CSV_ERR_IO_WRITE: + return "write error"; + default: + return "unknown error"; + } + // NOT REACHED +} diff --git a/tests/makefile b/tests/makefile new file mode 100644 index 0000000..e69de29 diff --git a/tests/src/csv-test.c b/tests/src/csv-test.c new file mode 100644 index 0000000..c99c049 --- /dev/null +++ b/tests/src/csv-test.c @@ -0,0 +1,627 @@ +#undef NDEBUG + +#include +#include +#include +#include +#include + +#include "csv.h" + +#define FMEMOPEN(f, data) \ + f = fmemopen(data, sizeof(data) - 1, "r"); \ + assert(f != NULL) + +#define FMEMCLOSE(f) \ + (void) fclose(f); \ + f = NULL + +#define UNUSED(x) (void) (x) + +void +test_empty_object(void) +{ + { + csv_t csv = { 0 }; + assert(csv_nfields(&csv) == 0); + } + + { + csv_t csv; + csv_init(&csv); + assert(csv_nfields(&csv) == 0); + } +} + +void +test_line_endings1(void) +{ + FILE *file = NULL; + csv_t csv = { 0 }; + + char data[] = "A,B,C\r\n" + "D,E,F\n"; + + FMEMOPEN(file, data); + + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "D") == 0); + assert(strcmp(csv_field(&csv, 1), "E") == 0); + assert(strcmp(csv_field(&csv, 2), "F") == 0); + + assert(csv_read(&csv, file) == 0); + + FMEMCLOSE(file); +} + +void +test_line_endings2(void) +{ + FILE *file = NULL; + csv_t csv = { 0 }; + + char data[] = "A,B,C\r\n" + "D,E,F"; + + FMEMOPEN(file, data); + + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "D") == 0); + assert(strcmp(csv_field(&csv, 1), "E") == 0); + assert(strcmp(csv_field(&csv, 2), "F") == 0); + + assert(csv_read(&csv, file) == 0); + + FMEMCLOSE(file); +} + +void +test_empty_fields(void) +{ + FILE *file = NULL; + csv_t csv = { 0 }; + + char data[] = "\n" + "\r" + "\r\n" + "\"\"\n" + "\"\"\r" + "\"\"\r\n" + ",\n" + ",\r" + ",\r\n" + "\"\",\"\"\n" + "\"\",\"\"\r" + "\"\",\"\"\r\n" + ",,\n" + ",,\r" + ",,\r\n" + "\"\",\"\",\n" + "\"\",\"\",\r" + "\"\",\"\",\r\n" + ",,"; + + FMEMOPEN(file, data); + + // Line 1 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "") == 0); + + // Line 2 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "") == 0); + + // Line 3 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "") == 0); + + // Line 4 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "") == 0); + + // Line 5 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "") == 0); + + // Line 6 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "") == 0); + + // Line 7 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + + // Line 8 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + + // Line 9 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + + // Line 10 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + + // Line 11 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + + // Line 12 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + + // Line 13 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + assert(strcmp(csv_field(&csv, 2), "") == 0); + + // Line 14 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + assert(strcmp(csv_field(&csv, 2), "") == 0); + + // Line 15 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + assert(strcmp(csv_field(&csv, 2), "") == 0); + + // Line 16 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + assert(strcmp(csv_field(&csv, 2), "") == 0); + + // Line 17 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + assert(strcmp(csv_field(&csv, 2), "") == 0); + + // Line 18 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + assert(strcmp(csv_field(&csv, 2), "") == 0); + + // Line 19 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "") == 0); + assert(strcmp(csv_field(&csv, 1), "") == 0); + assert(strcmp(csv_field(&csv, 2), "") == 0); + + // EOF + assert(csv_read(&csv, file) == 0); + + FMEMCLOSE(file); +} + +void +test_simple_fields(void) +{ + FILE *file = NULL; + csv_t csv = { 0 }; + + char data[] = "A\n" + "A\r" + "A\r\n" + "\"A\"\n" + "\"A\"\r" + "\"A\"\r\n" + "A,B\n" + "A,B\r" + "A,B\r\n" + "\"A\",\"B\"\n" + "\"A\",\"B\"\r" + "\"A\",\"B\"\r\n" + "A,B,C\n" + "A,B,C\r" + "A,B,C\r\n" + "\"A\",\"B\",C\n" + "\"A\",\"B\",C\r" + "\"A\",\"B\",C\r\n" + "A,B,C"; + + FMEMOPEN(file, data); + + // Line 1 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + + // Line 2 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + + // Line 3 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + + // Line 4 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + + // Line 5 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + + // Line 6 + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + + // Line 7 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + + // Line 8 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + + // Line 9 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + + // Line 10 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + + // Line 11 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + + // Line 12 + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + + // Line 13 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + // Line 14 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + // Line 15 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + // Line 16 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + // Line 17 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + // Line 18 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + // Line 19 + assert(csv_read(&csv, file) == 3); + assert(csv_nfields(&csv) == 3); + assert(strcmp(csv_field(&csv, 0), "A") == 0); + assert(strcmp(csv_field(&csv, 1), "B") == 0); + assert(strcmp(csv_field(&csv, 2), "C") == 0); + + // EOF + assert(csv_read(&csv, file) == 0); + + FMEMCLOSE(file); +} + +void +test_quoted_fields(void) +{ + FILE *file = NULL; + csv_t csv = { 0 }; + + char data[] = "foo \"baz\" bar,foo \"\"baz\"\" bar,\"foo \"\"baz\"\" bar\",\"foo \"baz\" bar\",\"foo \"\"baz\"\", bar\""; + + FMEMOPEN(file, data); + + assert(csv_read(&csv, file) == 5); + assert(csv_nfields(&csv) == 5); + assert(strcmp(csv_field(&csv, 0), "foo \"baz\" bar") == 0); + assert(strcmp(csv_field(&csv, 1), "foo \"\"baz\"\" bar") == 0); + assert(strcmp(csv_field(&csv, 2), "foo \"baz\" bar") == 0); + assert(strcmp(csv_field(&csv, 3), "foo \"baz\" bar") == 0); + assert(strcmp(csv_field(&csv, 4), "foo \"baz\", bar") == 0); + + assert(csv_read(&csv, file) == 0); + + FMEMCLOSE(file); +} + +void +test_wrong_quoted_field(void) +{ + FILE *file = NULL; + csv_t csv = { 0 }; + + char data[] = "\"foo"; + + FMEMOPEN(file, data); + + assert(csv_read(&csv, file) == 1); + assert(csv_nfields(&csv) == 1); + assert(strcmp(csv_field(&csv, 0), "foo") == 0); + + assert(csv_read(&csv, file) == 0); + + FMEMCLOSE(file); +} + +static void +cb_error(csv_err_t err, void *cb_arg) +{ + longjmp(*((jmp_buf *) cb_arg), err); +} + +void +test_out_of_range_error(void) +{ + jmp_buf env; + csv_options_t csv_options = csv_default_options; + csv_options.cb_error = cb_error; + csv_options.cb_error_arg = &env; + + csv_t csv; + csv_init_opt(&csv, &csv_options); + + FILE *file = NULL; + char data[] = ","; + + FMEMOPEN(file, data); + + switch ( setjmp(env) ) { + case CSV_ERR_OK: + assert(csv_read(&csv, file) == 2); + assert(csv_nfields(&csv) == 2); + + // force an "out of range"-Exception! + assert(strcmp(csv_field(&csv, 2), "will fail") == 0); + break; + + case CSV_ERR_OUT_OF_RANGE: + csv_cleanup(&csv); + break; + + default: + assert(!"this should not be happen!"); + } + + FMEMCLOSE(file); +} + +void +test_read_error(void) +{ + jmp_buf env; + csv_options_t csv_options = csv_default_options; + csv_options.cb_error = cb_error; + csv_options.cb_error_arg = &env; + + csv_t csv; + csv_init_opt(&csv, &csv_options); + + FILE *file = fopen("/dev/null", "r"); + assert(file != NULL); + (void) fputc('T', file); // Set Error-Flag! + + switch ( setjmp(env) ) { + case CSV_ERR_OK: + assert(csv_read(&csv, file) == 0); + break; + + case CSV_ERR_IO_READ: + csv_cleanup(&csv); + break; + + default: + assert(!"this should not be happen!"); + } + + (void) fclose(file); +} + +void * +my_allocate(size_t n, size_t size, void *cb_arg) +{ + UNUSED(cb_arg); + + void *ptr = calloc(n, size); + + return ptr; +} + +void * +my_allocate_null(size_t n, size_t size, void *cb_arg) +{ + UNUSED(n); + UNUSED(size); + UNUSED(cb_arg); + + return NULL; +} + +void * +my_reallocate(void *ptr, size_t n, size_t size, void *cb_arg) +{ + UNUSED(ptr); + UNUSED(n); + UNUSED(size); + UNUSED(cb_arg); + + return NULL; +} + +void +my_free(void *ptr, size_t n, size_t size, void *cb_arg) +{ + UNUSED(n); + UNUSED(size); + UNUSED(cb_arg); + + free(ptr); +} + +void +test_allocation_error1(void) +{ + jmp_buf env; + + csv_options_t csv_options = csv_default_options; + csv_options.cb_error = cb_error; + csv_options.cb_error_arg = &env; + csv_options.cb_allocate = my_allocate; + csv_options.cb_reallocate = my_reallocate; + csv_options.cb_free = my_free; + + csv_t csv; + csv_init_opt(&csv, &csv_options); + + char data[] = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T"; + + FILE *file = NULL; + FMEMOPEN(file, data); + assert(file != NULL); + + switch ( setjmp(env) ) { + case CSV_ERR_OK: + assert(csv_read(&csv, file) == 20); + assert(!"this should not be happen!"); + break; + + case CSV_ERR_OUT_OF_MEMORY: + csv_cleanup(&csv); + break; + + default: + assert(!"this should not be happen!"); + } + + FMEMCLOSE(file); +} + +void +show_version(void) +{ + const char *version = csv_version; + + printf("libcsv - version: "); + for ( ; *version; ++version ) { + putchar(*version); + } + + printf(", build date: "); + for ( ++version; *version; ++version ) { + putchar(*version); + } + + printf(", build time: "); + for ( ++version; *version; ++version ) { + putchar(*version); + } + putchar('\n'); +} + +int +main(void) +{ + show_version(); + + test_empty_object(); + test_line_endings1(); + test_line_endings2(); + test_empty_fields(); + test_simple_fields(); + test_quoted_fields(); + test_wrong_quoted_field(); + + test_out_of_range_error(); + test_read_error(); + + test_allocation_error1(); + + puts("all tests passed..."); + + return EXIT_SUCCESS; +} -- cgit v1.3