320 lines
9.9 KiB
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;
|
|
}
|