/* * The SPSS & SAS Viewer's reader: ReadStat (Evan Miller, MIT) behind a few * plain C functions. The page copies a file in and parses it; values are * kept column by column (numbers as doubles with a missing-value code, text * as UTF-8 with offsets) and the metadata - variables, labels, formats, * user-missing ranges, value-label sets, notes - as JSON. * * Missing codes: 0 a value, 1 system missing, 2 user-defined missing (the * value is kept), or the tag character of a tagged missing value (SAS .A-.Z * and ._, Stata .a-.z). */ #include #include #include #include #include #include #include "readstat.h" /* ── Input from memory ─────────────────────────────────────────────────── */ static uint8_t *in = NULL; static size_t in_len = 0, in_pos = 0; static int io_open(const char *path, void *io_ctx) { in_pos = 0; return 0; } static int io_close(void *io_ctx) { return 0; } static readstat_off_t io_seek(readstat_off_t off, readstat_io_flags_t whence, void *io_ctx) { long long base = whence == READSTAT_SEEK_SET ? 0 : whence == READSTAT_SEEK_CUR ? (long long)in_pos : (long long)in_len; long long next = base + off; if (next < 0 || next > (long long)in_len) return -1; in_pos = (size_t)next; return (readstat_off_t)next; } static ssize_t io_read(void *buf, size_t n, void *io_ctx) { size_t left = in_len - in_pos; size_t r = n < left ? n : left; memcpy(buf, in + in_pos, r); in_pos += r; return (ssize_t)r; } static readstat_error_t io_update(long file_size, readstat_progress_handler h, void *user_ctx, void *io_ctx) { return READSTAT_OK; } /* ── A growing text buffer (the JSON) ──────────────────────────────────── */ typedef struct { char *p; size_t len, cap; } sbuf; static void sb_put(sbuf *b, const char *s, size_t n) { if (b->len + n + 1 > b->cap) { size_t cap = b->cap ? b->cap * 2 : 4096; while (cap < b->len + n + 1) cap *= 2; b->p = (char *)realloc(b->p, cap); b->cap = cap; } memcpy(b->p + b->len, s, n); b->len += n; b->p[b->len] = 0; } static void sb_str(sbuf *b, const char *s) { sb_put(b, s, strlen(s)); } static void sb_json(sbuf *b, const char *s) { sb_put(b, "\"", 1); for (const unsigned char *c = (const unsigned char *)(s ? s : ""); *c; c++) { char esc[8]; if (*c == '"') sb_put(b, "\\\"", 2); else if (*c == '\\') sb_put(b, "\\\\", 2); else if (*c < 0x20) { snprintf(esc, sizeof esc, "\\u%04x", *c); sb_put(b, esc, 6); } else sb_put(b, (const char *)c, 1); } sb_put(b, "\"", 1); } static void sb_num(sbuf *b, double v) { char t[40]; if (isnan(v) || isinf(v)) { sb_str(b, "null"); return; } snprintf(t, sizeof t, "%.17g", v); sb_str(b, t); } /* ── Columns ───────────────────────────────────────────────────────────── */ typedef struct { int is_str; double *num; uint8_t *miss; uint32_t *off; /* strings: start of row i; off[rows] is the end */ char *str; size_t str_len, str_cap; int filled; /* rows written so far */ } column; static column *cols = NULL; static int ncols = 0, rows_cap = 0, nrows = 0; static sbuf vars = {0}, labels = {0}, notes = {0}, meta = {0}; static int nvars_json = 0, nnotes = 0; static char last_set[256] = ""; static int file_rows = -1; static char file_label[1024] = "", file_encoding[64] = ""; static void grow_rows(int need) { if (need <= rows_cap) return; int cap = rows_cap ? rows_cap : 1024; while (cap < need) cap *= 2; for (int i = 0; i < ncols; i++) { column *c = &cols[i]; c->miss = (uint8_t *)realloc(c->miss, cap); if (c->is_str) c->off = (uint32_t *)realloc(c->off, (cap + 1) * sizeof(uint32_t)); else c->num = (double *)realloc(c->num, cap * sizeof(double)); } rows_cap = cap; } /* Rows a column skipped (none in practice) become empty and missing. */ static void fill_to(column *c, int row) { while (c->filled < row) { c->miss[c->filled] = 1; if (c->is_str) c->off[c->filled] = (uint32_t)c->str_len; else c->num[c->filled] = NAN; c->filled++; } } static void free_all(void) { for (int i = 0; i < ncols; i++) { free(cols[i].num); free(cols[i].miss); free(cols[i].off); free(cols[i].str); } free(cols); cols = NULL; ncols = rows_cap = nrows = 0; free(vars.p); free(notes.p); free(meta.p); vars = (sbuf){0}; notes = (sbuf){0}; meta = (sbuf){0}; nvars_json = nnotes = 0; file_rows = -1; file_label[0] = file_encoding[0] = 0; } /* ── Handlers ──────────────────────────────────────────────────────────── */ static int on_metadata(readstat_metadata_t *m, void *ctx) { file_rows = readstat_get_row_count(m); const char *l = readstat_get_file_label(m); const char *e = readstat_get_file_encoding(m); snprintf(file_label, sizeof file_label, "%s", l ? l : ""); snprintf(file_encoding, sizeof file_encoding, "%s", e ? e : ""); return READSTAT_HANDLER_OK; } static int on_note(int i, const char *note, void *ctx) { sb_str(¬es, nnotes++ ? "," : ""); sb_json(¬es, note); return READSTAT_HANDLER_OK; } static void value_json(sbuf *b, readstat_value_t v) { if (readstat_value_type_class(v) == READSTAT_TYPE_CLASS_STRING) sb_json(b, readstat_string_value(v)); else sb_num(b, readstat_double_value(v)); } static int on_variable(int index, readstat_variable_t *v, const char *val_labels, void *ctx) { cols = (column *)realloc(cols, (ncols + 1) * sizeof(column)); column *c = &cols[ncols]; memset(c, 0, sizeof *c); readstat_type_t t = readstat_variable_get_type(v); c->is_str = t == READSTAT_TYPE_STRING || t == READSTAT_TYPE_STRING_REF; if (rows_cap) { c->miss = (uint8_t *)malloc(rows_cap); if (c->is_str) c->off = (uint32_t *)malloc((rows_cap + 1) * sizeof(uint32_t)); else c->num = (double *)malloc(rows_cap * sizeof(double)); } ncols++; sb_str(&vars, nvars_json++ ? ",{\"name\":" : "{\"name\":"); sb_json(&vars, readstat_variable_get_name(v)); sb_str(&vars, ",\"label\":"); sb_json(&vars, readstat_variable_get_label(v)); sb_str(&vars, ",\"format\":"); sb_json(&vars, readstat_variable_get_format(v)); sb_str(&vars, c->is_str ? ",\"type\":\"string\"" : ",\"type\":\"number\""); sb_str(&vars, ",\"labels\":"); sb_json(&vars, val_labels); char w[32]; snprintf(w, sizeof w, ",\"width\":%d", readstat_variable_get_display_width(v)); sb_str(&vars, w); sb_str(&vars, ",\"missing\":["); int n = readstat_variable_get_missing_ranges_count(v); for (int i = 0; i < n; i++) { sb_str(&vars, i ? ",[" : "["); value_json(&vars, readstat_variable_get_missing_range_lo(v, i)); sb_str(&vars, ","); value_json(&vars, readstat_variable_get_missing_range_hi(v, i)); sb_str(&vars, "]"); } sb_str(&vars, "]}"); return READSTAT_HANDLER_OK; } static int on_value(int obs, readstat_variable_t *v, readstat_value_t value, void *ctx) { int i = readstat_variable_get_index(v); if (i < 0 || i >= ncols) return READSTAT_HANDLER_OK; if (obs + 1 > rows_cap) grow_rows(obs + 1); column *c = &cols[i]; fill_to(c, obs); if (obs + 1 > nrows) nrows = obs + 1; uint8_t miss = 0; if (readstat_value_is_system_missing(value)) miss = 1; else if (readstat_value_is_tagged_missing(value)) miss = (uint8_t)readstat_value_tag(value); else if (readstat_value_is_defined_missing(value, v)) miss = 2; c->miss[obs] = miss; if (c->is_str) { const char *s = readstat_string_value(value); size_t n = s ? strlen(s) : 0; if (c->str_len + n > c->str_cap) { size_t cap = c->str_cap ? c->str_cap * 2 : 4096; while (cap < c->str_len + n) cap *= 2; c->str = (char *)realloc(c->str, cap); c->str_cap = cap; } c->off[obs] = (uint32_t)c->str_len; if (n) memcpy(c->str + c->str_len, s, n); c->str_len += n; } else { /* System and tagged missing have no value; user-defined missing keeps it. */ c->num[obs] = miss == 1 || miss > 2 ? NAN : readstat_double_value(value); } c->filled = obs + 1; return READSTAT_HANDLER_OK; } /* Value labels arrive grouped by set: {"set":[[value,"label"],...],...}. */ static int nlabelsets = 0; static int on_value_label(const char *set, readstat_value_t value, const char *label, void *ctx) { if (strcmp(set, last_set) != 0) { sb_str(&labels, last_set[0] ? "]," : ""); sb_json(&labels, set); sb_str(&labels, ":["); snprintf(last_set, sizeof last_set, "%s", set); nlabelsets++; } else sb_str(&labels, ","); sb_str(&labels, "["); if (readstat_value_is_tagged_missing(value)) { char t[8] = {'"', '.', readstat_value_tag(value), '"', 0}; sb_str(&labels, t); } else value_json(&labels, value); sb_str(&labels, ","); sb_json(&labels, label); sb_str(&labels, "]"); return READSTAT_HANDLER_OK; } static char error_text[512] = ""; static void on_error(const char *msg, void *ctx) { snprintf(error_text, sizeof error_text, "%s", msg); } static readstat_parser_t *new_parser(void) { readstat_parser_t *p = readstat_parser_init(); readstat_set_open_handler(p, io_open); readstat_set_close_handler(p, io_close); readstat_set_seek_handler(p, io_seek); readstat_set_read_handler(p, io_read); readstat_set_update_handler(p, io_update); readstat_set_error_handler(p, on_error); return p; } /* ── Entry points ──────────────────────────────────────────────────────── */ EMSCRIPTEN_KEEPALIVE uint8_t *rs_input(size_t len) { free(in); in = (uint8_t *)malloc(len ? len : 1); in_len = len; in_pos = 0; return in; } /* Starts a new file: forgets the previous one, including value labels from a catalog. */ EMSCRIPTEN_KEEPALIVE void rs_reset(void) { free_all(); free(labels.p); labels = (sbuf){0}; last_set[0] = 0; nlabelsets = 0; } /* Reads the value labels of a SAS catalog (.sas7bcat) in the input, to go with a .sas7bdat read next. */ EMSCRIPTEN_KEEPALIVE int rs_parse_catalog(void) { readstat_parser_t *p = new_parser(); readstat_set_value_label_handler(p, on_value_label); error_text[0] = 0; readstat_error_t e = readstat_parse_sas7bcat(p, "catalog", NULL); readstat_parser_free(p); return (int)e; } /* Parses the input. format: 0 SPSS .sav/.zsav, 1 SPSS .por, 2 SAS .sas7bdat, 3 SAS transport .xpt, 4 Stata .dta. Returns a ReadStat error code (0 is success). */ EMSCRIPTEN_KEEPALIVE int rs_parse(int format, const char *encoding) { free_all(); readstat_parser_t *p = new_parser(); readstat_set_metadata_handler(p, on_metadata); readstat_set_note_handler(p, on_note); readstat_set_variable_handler(p, on_variable); readstat_set_value_handler(p, on_value); readstat_set_value_label_handler(p, on_value_label); if (encoding && encoding[0]) readstat_set_file_character_encoding(p, encoding); error_text[0] = 0; readstat_error_t e; switch (format) { case 0: e = readstat_parse_sav(p, "file", NULL); break; case 1: e = readstat_parse_por(p, "file", NULL); break; case 2: e = readstat_parse_sas7bdat(p, "file", NULL); break; case 3: e = readstat_parse_xport(p, "file", NULL); break; case 4: e = readstat_parse_dta(p, "file", NULL); break; default: e = READSTAT_ERROR_PARSE; } readstat_parser_free(p); if (e == READSTAT_OK) { if (nrows > rows_cap) grow_rows(nrows); for (int i = 0; i < ncols; i++) { if (!rows_cap) grow_rows(1); fill_to(&cols[i], nrows); if (cols[i].is_str) cols[i].off[nrows] = (uint32_t)cols[i].str_len; } } free(meta.p); meta = (sbuf){0}; char t[64]; sb_str(&meta, "{\"rows\":"); snprintf(t, sizeof t, "%d", nrows); sb_str(&meta, t); sb_str(&meta, ",\"label\":"); sb_json(&meta, file_label); sb_str(&meta, ",\"encoding\":"); sb_json(&meta, file_encoding); sb_str(&meta, ",\"vars\":["); if (vars.p) sb_str(&meta, vars.p); sb_str(&meta, "],\"labelSets\":{"); if (labels.p) sb_str(&meta, labels.p); if (nlabelsets) sb_str(&meta, "]"); sb_str(&meta, "},\"notes\":["); if (notes.p) sb_str(&meta, notes.p); sb_str(&meta, "],\"error\":"); sb_json(&meta, e == READSTAT_OK ? "" : (error_text[0] ? error_text : readstat_error_message(e))); sb_str(&meta, "}"); return (int)e; } EMSCRIPTEN_KEEPALIVE const char *rs_meta(void) { return meta.p ? meta.p : "{}"; } EMSCRIPTEN_KEEPALIVE int rs_rows(void) { return nrows; } EMSCRIPTEN_KEEPALIVE int rs_is_string(int i) { return i >= 0 && i < ncols ? cols[i].is_str : 0; } EMSCRIPTEN_KEEPALIVE double *rs_numbers(int i) { return cols[i].num; } EMSCRIPTEN_KEEPALIVE uint8_t *rs_missing(int i) { return cols[i].miss; } EMSCRIPTEN_KEEPALIVE uint32_t *rs_offsets(int i) { return cols[i].off; } EMSCRIPTEN_KEEPALIVE char *rs_text(int i) { return cols[i].str; } EMSCRIPTEN_KEEPALIVE size_t rs_text_length(int i) { return cols[i].str_len; }