#include "csv.h" #include #include #include #include // === Compiletime Options === #ifndef CSV_DEFAULT_DELIMITER # define CSV_DEFAULT_DELIMITER '"' #endif #ifndef CSV_DEFAULT_SEPARATOR # define CSV_DEFAULT_SEPARATOR ',' #endif // === some usefull Makros === #define UNUSED(x) (void) (sizeof((x), 0)) // 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 === #if ( (defined(__GNUC__) && __GNUC__ >= 5) || (defined(__clang__) && (__clang_major__ > 3 || (__clang_major__ == 3 && __clang_minor__ >= 4))) ) # define mul_overflow __builtin_mul_overflow #else static inline int mul_overflow(size_t a, size_t b, size_t *out) { *out = a * b; return (a != 0 && (*out / a) != b); } #endif static void * csv_mem_allocate(size_t n, size_t sz, void *cb_arg) { UNUSED(cb_arg); size_t len; if ( n == 0 || sz == 0 || mul_overflow(n, sz, &len) ) { return NULL; } return malloc(len); } static void * csv_mem_reallocate(void *ptr, size_t n, size_t sz, void *cb_arg) { UNUSED(cb_arg); size_t len; if ( n == 0 || sz == 0 || mul_overflow(n, sz, &len) ) { return NULL; } return realloc(ptr, len); } static void csv_mem_free(void *ptr, size_t n, size_t sz, void *cb_arg) { UNUSED(n); UNUSED(sz); 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_allocate = csv_mem_allocate, .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); } 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 void csv_string_append(csv_string_t *csv_string, char ch, const csv_options_t *const csv_options) { assert(csv_string != NULL); assert(csv_options != NULL); static const size_t INITIAL_CAP = 16; if ( csv_string->pos == csv_string->cap ) { // grow if needed if ( csv_string->str == NULL ) { // first call? if ( (csv_string->str = csv_options->cb_allocate(INITIAL_CAP, 1, csv_options->cb_memory_arg)) == NULL ) { csv_fatal_error(CSV_ERR_OUT_OF_MEMORY, csv_options); return; } csv_string->cap = INITIAL_CAP; } else { // subsequent call size_t cap = (csv_string->cap * 3) / 2; // *= 1.5 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; } } csv_string->str[csv_string->pos++] = 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_append(csv_field_t *csv_field, size_t idx, const csv_options_t *const csv_options) { assert(csv_field != NULL); assert(csv_options != NULL); static const size_t INITIAL_CAP = 16; if ( csv_field->pos == csv_field->cap ) { // grow if needed if ( csv_field->fields == NULL ) { // first call if ( (csv_field->fields = csv_options->cb_allocate(INITIAL_CAP, sizeof(csv_field->fields[0]), csv_options->cb_memory_arg)) == NULL ) { csv_fatal_error(CSV_ERR_OUT_OF_MEMORY, csv_options); return; } csv_field->cap = INITIAL_CAP; } else { // subsequent call size_t cap = (csv_field->cap * 3) / 2; // *= 1.5 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; } } csv_field->fields[csv_field->pos++] = idx; // append idx } 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 < 0 || 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 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' ) { 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); 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' ) { 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 }