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 )