calog/libs/calogCsv.c

320 lines
9.9 KiB
C

// 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 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;
}