File-Raw-Separated
view release on metacpan or search on metacpan
separated_parser.c view on Meta::CPAN
/*
* separated_parser.c - CSV/TSV state machine for File::Raw::Separated.
*
* See include/separated_parser.h for the public contract.
*/
#include "separated_parser.h"
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* ------------------------------------------------------------
* Internal types
* ------------------------------------------------------------ */
typedef enum {
ST_START_FIELD = 0,
ST_IN_UNQUOTED,
ST_IN_QUOTED,
ST_MAYBE_END_QUOTE
} parse_state_t;
struct separated_ctx {
/* Resolved options (copied at init time). */
separated_options_t opts;
/* Caller. */
separated_field_cb cb;
void *ud;
/* Field buffer (geometric growth). */
char *buf;
size_t buf_len;
size_t buf_cap;
/* State. */
parse_state_t state;
int field_was_quoted; /* 1 if current field began with a quote */
int bom_checked; /* 1 once we've decided about the BOM */
int any_field_in_row; /* 1 if at least one field started in this row */
/* Auto-detected / pinned EOL. */
separated_eol_t detected_eol;
int pending_cr; /* 1 if last byte was CR awaiting LF/data (CRLF detect) */
/* Diagnostics. */
size_t bytes_consumed;
size_t rows_emitted;
size_t err_offset;
/* Sticky error: once non-zero, all _feed/_finish are no-ops. */
separated_err_t sticky_err;
};
/* Effective max-field cap (resolves opts.max_field_len == 0 to default). */
static size_t
effective_field_cap(const separated_options_t *opts)
{
return opts->max_field_len ? opts->max_field_len
: SEPARATED_FIELD_DEFAULT_CAP;
}
/* ------------------------------------------------------------
* Field buffer: geometric growth with hard cap
* ------------------------------------------------------------ */
static separated_err_t
buf_putc(separated_ctx_t *ctx, char c)
{
if (ctx->buf_len + 1 > ctx->buf_cap) {
size_t new_cap = ctx->buf_cap ? ctx->buf_cap * 2 : 64;
size_t cap_max = effective_field_cap(&ctx->opts);
char *new_buf;
if (new_cap > cap_max) new_cap = cap_max;
if (new_cap <= ctx->buf_len) {
return SEPARATED_ERR_FIELD_TOO_LONG;
}
new_buf = (char *)realloc(ctx->buf, new_cap);
if (!new_buf) return SEPARATED_ERR_NOMEM;
ctx->buf = new_buf;
ctx->buf_cap = new_cap;
}
ctx->buf[ctx->buf_len++] = c;
return SEPARATED_OK;
}
static void
buf_reset(separated_ctx_t *ctx)
{
ctx->buf_len = 0;
}
/* ------------------------------------------------------------
* Trim helper for unquoted fields when opts.trim is on.
* Strips only ASCII space and tab. Quoted fields are never trimmed.
* ------------------------------------------------------------ */
static void
trim_buf(char *buf, size_t *plen)
{
size_t len = *plen;
size_t start = 0;
size_t end;
while (start < len && (buf[start] == ' ' || buf[start] == '\t')) start++;
end = len;
while (end > start && (buf[end - 1] == ' ' || buf[end - 1] == '\t')) end--;
if (start > 0) memmove(buf, buf + start, end - start);
*plen = end - start;
}
/* ------------------------------------------------------------
* Emit a field/row to the callback.
* end_of_row=1 means "this field is the last in its row".
* ------------------------------------------------------------ */
static separated_err_t
emit_field(separated_ctx_t *ctx, int end_of_row)
{
int call_rc;
/* Trim only on unquoted fields. */
if (ctx->opts.trim && !ctx->field_was_quoted) {
trim_buf(ctx->buf, &ctx->buf_len);
}
if (ctx->opts.empty_is_undef
&& !ctx->field_was_quoted
&& ctx->buf_len == 0) {
call_rc = ctx->cb(NULL, SEPARATED_FIELD_NULL_LEN,
end_of_row, ctx->ud);
} else {
/* Pass even an empty quoted field as a real "" field. */
const char *p = ctx->buf_len ? ctx->buf : "";
call_rc = ctx->cb(p, ctx->buf_len, end_of_row, ctx->ud);
}
if (call_rc != 0) return SEPARATED_ERR_ABORTED;
buf_reset(ctx);
ctx->field_was_quoted = 0;
ctx->any_field_in_row = 1;
if (end_of_row) {
ctx->rows_emitted++;
ctx->any_field_in_row = 0;
}
return SEPARATED_OK;
}
/* ------------------------------------------------------------
* BOM stripping (UTF-8 only, when binary=0).
* Called once before any byte is interpreted. Caller passes the
* incoming buffer pointer + length pair through bom_skip; on return
* any leading 3-byte BOM has been advanced past.
* ------------------------------------------------------------ */
static void
bom_check(separated_ctx_t *ctx, const char **pp, size_t *plen)
{
if (ctx->bom_checked) return;
ctx->bom_checked = 1;
if (ctx->opts.binary) return;
if (*plen >= 3) {
const unsigned char *u = (const unsigned char *)*pp;
if (u[0] == 0xEF && u[1] == 0xBB && u[2] == 0xBF) {
*pp += 3;
*plen -= 3;
ctx->bytes_consumed += 3;
}
}
}
/* ------------------------------------------------------------
* EOL helpers
*
* detect_or_match returns 1 if the byte at `c` (with the parser's
* pending_cr flag) is a row terminator under the active EOL mode,
* 0 if it's a normal byte, or a negative error code on a pinned
* mismatch under strict mode.
*
* On a successful match the function may consume the byte (we always
* do â the caller treats the return-1 case as "row ended here") and
* also flips pending_cr or detected_eol as appropriate.
*
* NOTE: CRLF handling needs lookahead-of-1. We model it with the
* pending_cr bit:
* see CR => set pending_cr=1, do NOT emit row yet.
* next byte:
* if LF => CRLF row terminator, clear pending_cr.
* else => emit deferred CR-row terminator (CR mode), then
* re-process current byte from scratch.
*
* Keeping that in a tiny helper keeps the main loop legible.
* ------------------------------------------------------------ */
/* Return 1 if c is a terminator after the active EOL mode considers it. */
static int
is_lf(int c) { return c == '\n'; }
static int
is_cr(int c) { return c == '\r'; }
/* ------------------------------------------------------------
* Core feed loop.
*
* Drives the state machine over [buf, buf+len). Returns OK or the
* first error encountered; on error err_offset is set to the byte
* offset within the original input (ctx->bytes_consumed at the
* point of failure).
* ------------------------------------------------------------ */
#define FAIL(code) do { \
ctx->sticky_err = (code); \
( run in 0.771 second using v1.01-cache-2.11-cpan-9581c071862 )