Packages
glazer
0.3.2
0.5.17
0.5.16
0.5.15
0.5.14
retired
0.5.13
retired
0.5.12
retired
0.5.11
retired
0.5.10
retired
0.5.9
retired
0.5.8
retired
0.5.7
retired
0.5.6
retired
0.5.5
retired
0.5.4
retired
0.5.3
retired
0.5.2
retired
0.5.1
retired
0.5.0
retired
0.3.2
retired
0.3.0
retired
0.2.6
retired
0.2.5
retired
0.2.4
retired
0.2.3
retired
0.2.2
retired
0.2.1
retired
0.2.0
retired
0.1.7
retired
0.1.6
retired
0.1.5
retired
0.1.4
retired
Erlang NIF JSON encoder/decoder using the glaze C++ library
Retired package: Deprecated - Deprecated
Current section
Files
Jump to
Current section
Files
c_src/glazer_csv.hpp
// vim:ts=2:sw=2:et
// ---------------------------------------------------------------------------
// CSV decoding/encoding (RFC 4180-style), producing/consuming native Erlang
// terms in a single pass.
//
// Decoding:
// - Without `headers`: a list of rows, each row a list of binary fields.
// - With `headers`: the first row becomes the column names, and each
// subsequent row decodes to a map (or proplist) keyed by those names.
//
// Encoding accepts the same shapes: a list of rows (each row a list of
// binaries/atoms/integers/floats), or — with `headers` — a list of maps
// whose values are written out in the given column order.
//
// Quoting follows RFC 4180: a field is quoted if it contains the delimiter,
// a quote character, or a line break; embedded quotes are doubled.
// ---------------------------------------------------------------------------
#pragma once
#include <string>
#include <string_view>
#include <vector>
#include <erl_nif.h>
#include "glazer_atoms.hpp"
#include "glazer_bigint.hpp"
#include "glazer_common.hpp"
// ---------------------------------------------------------------------------
// Options
// ---------------------------------------------------------------------------
struct CsvDecodeOpts {
char delimiter = ',';
bool headers = false;
bool label_atom = false;
bool label_existing_atom = false;
};
static bool parse_csv_decode_opts(ErlNifEnv* env, ERL_NIF_TERM list, CsvDecodeOpts& opts)
{
ERL_NIF_TERM head, tail = list;
while (enif_get_list_cell(env, tail, &head, &tail)) {
if (enif_is_identical(head, AM_HEADERS)) { opts.headers = true; continue; }
int arity; const ERL_NIF_TERM* tp;
if (!enif_get_tuple(env, head, &arity, &tp) || arity != 2) continue;
if (enif_is_identical(tp[0], AM_DELIMITER)) {
int cp;
if (!enif_get_int(env, tp[1], &cp) || cp <= 0 || cp > 0x7F) return false;
opts.delimiter = static_cast<char>(cp);
} else if (enif_is_identical(tp[0], AM_KEYS)) {
if (enif_is_identical(tp[1], AM_LABEL_ATOM)) opts.label_atom = true;
else if (enif_is_identical(tp[1], AM_LABEL_EXISTING_ATOM)) opts.label_existing_atom = true;
else if (enif_is_identical(tp[1], AM_LABEL_BINARY)) { opts.label_atom = false; opts.label_existing_atom = false; }
}
}
return true;
}
struct CsvEncodeOpts {
char delimiter = ',';
bool headers = false;
std::string_view line_ending = "\r\n";
};
static bool parse_csv_encode_opts(ErlNifEnv* env, ERL_NIF_TERM list, CsvEncodeOpts& opts)
{
ERL_NIF_TERM head, tail = list;
while (enif_get_list_cell(env, tail, &head, &tail)) {
if (enif_is_identical(head, AM_HEADERS)) { opts.headers = true; continue; }
int arity; const ERL_NIF_TERM* tp;
if (!enif_get_tuple(env, head, &arity, &tp) || arity != 2) continue;
if (enif_is_identical(tp[0], AM_DELIMITER)) {
int cp;
if (!enif_get_int(env, tp[1], &cp) || cp <= 0 || cp > 0x7F) return false;
opts.delimiter = static_cast<char>(cp);
} else if (enif_is_identical(tp[0], AM_LINE_ENDING)) {
if (enif_is_identical(tp[1], AM_LF)) opts.line_ending = "\n";
else if (enif_is_identical(tp[1], AM_CRLF)) opts.line_ending = "\r\n";
else return false;
}
}
return true;
}
// ---------------------------------------------------------------------------
// Decoder
// ---------------------------------------------------------------------------
struct CsvDecoder {
ErlNifEnv* m_env;
const CsvDecodeOpts& m_opts;
const char* m_p;
const char* m_end;
CsvDecoder(ErlNifEnv* e, const CsvDecodeOpts& o, const char* data, size_t size)
: m_env(e), m_opts(o), m_p(data), m_end(data + size) {}
static bool is_eol(char c) { return c == '\n' || c == '\r'; }
void skip_eol() {
if (m_p < m_end && *m_p == '\r') ++m_p;
if (m_p < m_end && *m_p == '\n') ++m_p;
}
// Unescapes doubled quotes by copying the runs *between* them in bulk,
// rather than byte-by-byte — for a field with a handful of escaped quotes
// this is a handful of memcpy calls instead of one push_back per byte.
ERL_NIF_TERM make_field_term(std::string_view sv, bool has_quote_escape)
{
if (!has_quote_escape) return make_binary(m_env, sv);
std::string buf;
buf.reserve(sv.size());
size_t start = 0;
for (size_t i = 0; i < sv.size(); ++i) {
if (sv[i] == '"' && i + 1 < sv.size() && sv[i+1] == '"') {
buf.append(sv.data() + start, i + 1 - start); // include first quote
start = i + 2; // skip the second quote
++i;
}
}
buf.append(sv.data() + start, sv.size() - start);
return make_binary(m_env, buf);
}
// Reads one field starting at m_p. Advances m_p past the field (not past
// the following delimiter/EOL). Sets `eol`/`eof` flags for the caller and
// `more_fields` to false if the field was the last one on the line.
ERL_NIF_TERM read_field(bool& ok)
{
ok = true;
if (m_p < m_end && *m_p == '"') {
++m_p; // opening quote
const char* start = m_p;
bool has_escape = false;
for (;;) {
if (m_p >= m_end) { ok = false; return 0; } // unterminated quoted field
if (*m_p == '"') {
if (m_p + 1 < m_end && m_p[1] == '"') { has_escape = true; m_p += 2; continue; }
break; // closing quote
}
++m_p;
}
std::string_view sv(start, static_cast<size_t>(m_p - start));
++m_p; // closing quote
return make_field_term(sv, has_escape);
}
const char* start = m_p;
while (m_p < m_end && *m_p != m_opts.delimiter && !is_eol(*m_p)) ++m_p;
return make_binary(m_env, std::string_view(start, static_cast<size_t>(m_p - start)));
}
// Reads one record (row) into `fields`. Returns false at end of input with
// no fields read (clean EOF). On a malformed quoted field, sets `err`.
template<size_t N>
bool read_record(SmallTermVec<N>& fields, bool& err)
{
err = false;
fields.set_size(0);
if (m_p >= m_end) return false;
// Skip blank lines.
while (m_p < m_end && is_eol(*m_p)) skip_eol();
if (m_p >= m_end) return false;
for (;;) {
bool ok;
ERL_NIF_TERM field = read_field(ok);
if (!ok) [[unlikely]] { err = true; return false; }
fields.push_back(field);
if (m_p < m_end && *m_p == m_opts.delimiter) { ++m_p; continue; }
if (m_p < m_end && is_eol(*m_p)) { skip_eol(); }
break;
}
return true;
}
ERL_NIF_TERM make_header_key(ERL_NIF_TERM bin_term)
{
if (!m_opts.label_atom && !m_opts.label_existing_atom) return bin_term;
ErlNifBinary bin;
if (!enif_inspect_binary(m_env, bin_term, &bin)) return bin_term;
auto sv = std::string_view(reinterpret_cast<const char*>(bin.data), bin.size);
if (m_opts.label_atom)
return enif_make_atom_len(m_env, sv.data(), sv.size());
ERL_NIF_TERM t;
return enif_make_existing_atom_len(m_env, sv.data(), sv.size(), &t, ERL_NIF_LATIN1)
? t : bin_term;
}
// Decodes the entire input into a list of rows (maps if `headers`).
// Returns:
// - success: {true, Result :: list(map) | list(list)}
// - failure: {false, Result :: atom | binary}
std::tuple<bool, ERL_NIF_TERM> decode()
{
SmallTermVec<64> fields;
SmallTermVec<64> header;
std::vector<ERL_NIF_TERM> rows;
bool err = false;
auto rec = read_record(fields, err);
if (!rec) [[unlikely]]
goto DONE;
// If `headers` is set, the first record becomes the column names.
// Otherwise, treat it as a data row and decode it as usual.
if (m_opts.headers) {
for (auto field : fields)
header.push_back(make_header_key(field));
rec = read_record(fields, err);
}
if (m_opts.headers) {
while (rec) {
auto map = fields.to_erl_map(m_env, header);
if (!map) [[unlikely]]
return std::make_tuple(false, AM_DUPLICATE_HEADER);
rows.push_back(map);
rec = read_record(fields, err);
}
} else {
do { rows.push_back(fields.to_erl_list(m_env)); }
while (read_record(fields, err));
}
DONE:
if (err) [[unlikely]]
return std::make_tuple(false, AM_UNTERMINATED_QUOTED_FIELD);
return std::make_tuple(true,
enif_make_list_from_array(m_env, rows.data(), unsigned(rows.size())));
}
};
// ---------------------------------------------------------------------------
// Encoder
// ---------------------------------------------------------------------------
struct CsvEncoder {
ErlNifEnv* m_env;
const CsvEncodeOpts& m_opts;
OutBuf& m_out;
std::string m_err;
CsvEncoder(ErlNifEnv* e, const CsvEncodeOpts& o, OutBuf& out)
: m_env(e), m_opts(o), m_out(out) {}
void push_field_raw(std::string_view sv)
{
bool needs_quotes = false;
for (char c : sv) {
if (c == m_opts.delimiter || c == '"' || c == '\n' || c == '\r') { needs_quotes = true; break; }
}
if (!needs_quotes) { m_out.push(sv); return; }
m_out.push('"');
size_t start = 0;
for (size_t i = 0; i < sv.size(); ++i) {
if (sv[i] == '"') {
m_out.push(sv.data() + start, i - start + 1);
m_out.push('"'); // double the quote
start = i + 1;
}
}
m_out.push(sv.data() + start, sv.size() - start);
m_out.push('"');
}
// Encodes one field term (binary, atom, integer, or float). Returns false
// for unsupported term types.
bool encode_field(ERL_NIF_TERM term)
{
switch (enif_term_type(m_env, term)) {
case ERL_NIF_TERM_TYPE_BITSTRING: {
ErlNifBinary bin;
if (!enif_inspect_binary(m_env, term, &bin)) return false;
push_field_raw({reinterpret_cast<const char*>(bin.data), bin.size});
return true;
}
case ERL_NIF_TERM_TYPE_INTEGER:
return glazer::BigInt::encode(m_env, term, m_out);
case ERL_NIF_TERM_TYPE_FLOAT: {
double d;
if (!enif_get_double(m_env, term, &d)) return false;
char buf[32];
auto [e, ec] = std::to_chars(buf, buf+32, d, std::chars_format::general);
if (ec != std::errc{}) return false;
m_out.push(buf, e - buf);
return true;
}
case ERL_NIF_TERM_TYPE_ATOM: {
char buf[256];
std::string_view sv;
if (!atom_to_sv(m_env, term, buf, sizeof(buf), sv)) return false;
push_field_raw(sv);
return true;
}
default:
return false;
}
}
bool encode_row(ERL_NIF_TERM row)
{
ERL_NIF_TERM head, tail = row;
bool first = true;
while (enif_get_list_cell(m_env, tail, &head, &tail)) {
if (!first) m_out.push(m_opts.delimiter);
first = false;
if (!encode_field(head)) { m_err = "cannot encode CSV field"; return false; }
}
m_out.push(m_opts.line_ending);
return true;
}
bool encode_map_row(ERL_NIF_TERM map, const std::vector<ERL_NIF_TERM>& header)
{
for (size_t i = 0; i < header.size(); ++i) {
if (i > 0) m_out.push(m_opts.delimiter);
ERL_NIF_TERM val;
if (!enif_get_map_value(m_env, map, header[i], &val)) continue; // missing key -> empty field
if (!encode_field(val)) { m_err = "cannot encode CSV field"; return false; }
}
m_out.push(m_opts.line_ending);
return true;
}
// `term` is a list of rows (each a list of fields), or — with `headers` —
// a list of maps.
bool encode(ERL_NIF_TERM term)
{
if (!enif_is_list(m_env, term)) { m_err = "expected a list of rows"; return false; }
if (enif_is_empty_list(m_env, term)) return true;
if (m_opts.headers) {
// Determine column order from the first row's map keys.
ERL_NIF_TERM head, tail = term;
enif_get_list_cell(m_env, tail, &head, &tail);
if (enif_term_type(m_env, head) != ERL_NIF_TERM_TYPE_MAP) {
m_err = "headers option requires rows to be maps";
return false;
}
std::vector<ERL_NIF_TERM> header;
ERL_NIF_TERM key, val;
ErlNifMapIterator it;
enif_map_iterator_create(m_env, head, &it, ERL_NIF_MAP_ITERATOR_FIRST);
while (enif_map_iterator_get_pair(m_env, &it, &key, &val)) {
header.push_back(key);
enif_map_iterator_next(m_env, &it);
}
enif_map_iterator_destroy(m_env, &it);
// Emit header row.
for (size_t i = 0; i < header.size(); ++i) {
if (i > 0) m_out.push(m_opts.delimiter);
if (!encode_field(header[i])) { m_err = "cannot encode CSV header"; return false; }
}
m_out.push(m_opts.line_ending);
ERL_NIF_TERM row, rest = term;
while (enif_get_list_cell(m_env, rest, &row, &rest)) {
if (enif_term_type(m_env, row) != ERL_NIF_TERM_TYPE_MAP) {
m_err = "headers option requires rows to be maps";
return false;
}
if (!encode_map_row(row, header)) return false;
}
return true;
}
ERL_NIF_TERM row, rest = term;
while (enif_get_list_cell(m_env, rest, &row, &rest)) {
if (!enif_is_list(m_env, row)) { m_err = "expected each row to be a list"; return false; }
if (!encode_row(row)) return false;
}
return true;
}
};