// calogCsv.c -- calog CSV library (see calogCsv.h). A hand-written RFC 4180 parser/formatter, no // dependency (the grammar is small, mirroring the in-tree JSON codec). Natives are inline. #include "calogCsv.h" #include "calogInternal.h" #include #include #include #include // A growable byte buffer for one cell / the output. typedef struct CsvBufT { char *data; size_t len; size_t cap; } CsvBufT; static int32_t csvBufByte(CsvBufT *buffer, char byte); static int32_t csvBufBytes(CsvBufT *buffer, const char *bytes, size_t length); static int32_t csvCellToText(const CalogValueT *cell, CsvBufT *out, char delimiter); static int32_t csvCommitRow(CalogAggT *rows, CalogAggT **rowPtr); static int32_t csvFormat(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t csvParse(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t csvPushField(CalogAggT *row, CsvBufT *field); int32_t calogCsvRegister(CalogT *calog) { int32_t status; status = calogRegisterInline(calog, "csvParse", csvParse, NULL); if (status != calogOkE) { return status; } return calogRegisterInline(calog, "csvFormat", csvFormat, NULL); } static int32_t csvBufByte(CsvBufT *buffer, char byte) { return csvBufBytes(buffer, &byte, 1); } static int32_t csvBufBytes(CsvBufT *buffer, const char *bytes, size_t length) { if (buffer->len + length > buffer->cap) { size_t wanted; char *grown; wanted = buffer->cap ? buffer->cap * 2 : 128; while (wanted < buffer->len + length) { wanted *= 2; } grown = (char *)realloc(buffer->data, wanted); if (grown == NULL) { return calogErrOomE; } buffer->data = grown; buffer->cap = wanted; } memcpy(buffer->data + buffer->len, bytes, length); buffer->len += length; return calogOkE; } // Render one cell into `out`, quoting per RFC 4180 if it contains the delimiter, a quote, CR, or LF. static int32_t csvCellToText(const CalogValueT *cell, CsvBufT *out, char delimiter) { char scratch[64]; const char *bytes; int64_t length; bool needQuote; int64_t i; switch (cell->type) { case calogNilE: return calogOkE; case calogBoolE: bytes = cell->as.b ? "true" : "false"; length = (int64_t)strlen(bytes); break; case calogIntE: length = snprintf(scratch, sizeof(scratch), "%lld", (long long)cell->as.i); bytes = scratch; break; case calogRealE: length = snprintf(scratch, sizeof(scratch), "%g", cell->as.r); bytes = scratch; break; case calogStringE: bytes = cell->as.s.bytes; length = cell->as.s.length; break; default: return calogErrTypeE; } needQuote = false; for (i = 0; i < length; i++) { char c; c = bytes[i]; if (c == delimiter || c == '"' || c == '\n' || c == '\r') { needQuote = true; break; } } if (!needQuote) { return csvBufBytes(out, bytes, (size_t)length); } if (csvBufByte(out, '"') != calogOkE) { return calogErrOomE; } for (i = 0; i < length; i++) { if (bytes[i] == '"' && csvBufByte(out, '"') != calogOkE) { return calogErrOomE; } if (csvBufByte(out, bytes[i]) != calogOkE) { return calogErrOomE; } } return csvBufByte(out, '"'); } static int32_t csvFormat(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CsvBufT out; char delimiter; int64_t rowIndex; int32_t status; (void)userData; calogValueNil(result); if (argCount < 1 || argCount > 2 || args[0].type != calogAggE) { return calogFail(result, calogErrArgE, "csvFormat expects (rows [, delimiter])"); } delimiter = ','; if (argCount == 2) { if (args[1].type != calogStringE || args[1].as.s.length != 1) { return calogFail(result, calogErrArgE, "csvFormat: delimiter must be a one-byte string"); } delimiter = args[1].as.s.bytes[0]; } memset(&out, 0, sizeof(out)); status = calogOkE; for (rowIndex = 0; rowIndex < args[0].as.agg->arrayCount; rowIndex++) { CalogValueT *row; int64_t cellIndex; if (rowIndex > 0 && csvBufBytes(&out, "\r\n", 2) != calogOkE) { status = calogErrOomE; break; } row = &args[0].as.agg->array[rowIndex]; if (row->type != calogAggE) { status = calogFail(result, calogErrArgE, "csvFormat: each row must be a list of cells"); break; } for (cellIndex = 0; cellIndex < row->as.agg->arrayCount; cellIndex++) { if (cellIndex > 0 && csvBufByte(&out, delimiter) != calogOkE) { status = calogErrOomE; break; } status = csvCellToText(&row->as.agg->array[cellIndex], &out, delimiter); if (status != calogOkE) { break; } } if (status != calogOkE) { break; } } if (status == calogOkE) { status = calogValueString(result, out.data ? out.data : "", (int64_t)out.len); } else if (result->type != calogStringE) { (void)calogFail(result, status, "csvFormat: failed to build output"); } free(out.data); return status; } // Wrap *rowPtr, push it into rows, and replace *rowPtr with a fresh empty row (NULL on failure so // the caller's cleanup never double-frees a row already owned by rows). static int32_t csvCommitRow(CalogAggT *rows, CalogAggT **rowPtr) { CalogValueT rowValue; int32_t status; calogValueAgg(&rowValue, *rowPtr); *rowPtr = NULL; status = calogAggPush(rows, &rowValue); if (status != calogOkE) { calogValueFree(&rowValue); return status; } status = calogAggCreate(rowPtr, calogListE); if (status != calogOkE) { *rowPtr = NULL; } return status; } // Push the accumulated field as a string cell into row, then reset the field. static int32_t csvPushField(CalogAggT *row, CsvBufT *field) { CalogValueT cell; int32_t status; status = calogValueString(&cell, field->data ? field->data : "", (int64_t)field->len); if (status != calogOkE) { return status; } status = calogAggPush(row, &cell); if (status != calogOkE) { calogValueFree(&cell); return status; } field->len = 0; return calogOkE; } static int32_t csvParse(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogAggT *rows; CalogAggT *row; CsvBufT field; const char *text; char delimiter; int64_t length; int64_t i; bool inQuotes; bool lineHasContent; int32_t status; (void)userData; calogValueNil(result); if (argCount < 1 || argCount > 2 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "csvParse expects (text [, delimiter])"); } delimiter = ','; if (argCount == 2) { if (args[1].type != calogStringE || args[1].as.s.length != 1) { return calogFail(result, calogErrArgE, "csvParse: delimiter must be a one-byte string"); } delimiter = args[1].as.s.bytes[0]; } status = calogAggCreate(&rows, calogListE); if (status != calogOkE) { return calogFail(result, status, "csvParse: out of memory"); } row = NULL; status = calogAggCreate(&row, calogListE); if (status != calogOkE) { calogAggFree(rows); return calogFail(result, status, "csvParse: out of memory"); } text = args[0].as.s.bytes; length = args[0].as.s.length; memset(&field, 0, sizeof(field)); inQuotes = false; lineHasContent = false; i = 0; while (status == calogOkE && i < length) { char c; c = text[i]; if (inQuotes) { if (c == '"') { if (i + 1 < length && text[i + 1] == '"') { status = csvBufByte(&field, '"'); i += 2; } else { inQuotes = false; i++; } } else { status = csvBufByte(&field, c); i++; } continue; } if (c == '"' && field.len == 0) { inQuotes = true; lineHasContent = true; i++; } else if (c == delimiter) { lineHasContent = true; status = csvPushField(row, &field); i++; } else if (c == '\n' || c == '\r') { if (c == '\r' && i + 1 < length && text[i + 1] == '\n') { i++; } if (lineHasContent) { status = csvPushField(row, &field); if (status == calogOkE) { status = csvCommitRow(rows, &row); } lineHasContent = false; } i++; } else { lineHasContent = true; status = csvBufByte(&field, c); i++; } } // Flush a final row that had no trailing newline. if (status == calogOkE && lineHasContent) { status = csvPushField(row, &field); if (status == calogOkE) { status = csvCommitRow(rows, &row); } } free(field.data); if (row != NULL) { calogAggFree(row); // an unused fresh row (trailing newline) or a partial row on error } if (status != calogOkE) { calogAggFree(rows); return calogFail(result, status, "csvParse: out of memory"); } calogValueAgg(result, rows); return calogOkE; }