@ -0,0 +1,4 @@ | |||||
*.beam | |||||
*.o | |||||
*.so | |||||
ebin/jiffy.app |
@ -0,0 +1,11 @@ | |||||
all: build | |||||
%.beam: %.erl | |||||
erlc -o test/ $< | |||||
build: c_src/decoder.c | |||||
./rebar compile | |||||
check: test/etap.beam test/util.beam | |||||
prove test/*.t |
@ -0,0 +1,934 @@ | |||||
#include <assert.h> | |||||
#include <errno.h> | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#include "erl_nif.h" | |||||
#include "jiffy.h" | |||||
#define U(c) ((unsigned char) (c)) | |||||
#define ERROR(i, msg) make_error(st, env, msg) | |||||
#define STACK_SIZE_INC 64 | |||||
#define NUM_BUF_LEN 256 | |||||
enum { | |||||
st_value=0, | |||||
st_object, | |||||
st_array, | |||||
st_key, | |||||
st_colon, | |||||
st_comma, | |||||
st_done, | |||||
st_invalid | |||||
} JsonState; | |||||
enum { | |||||
nst_init=0, | |||||
nst_sign, | |||||
nst_mantissa, | |||||
nst_frac0, | |||||
nst_frac1, | |||||
nst_frac, | |||||
nst_esign, | |||||
nst_edigit | |||||
} JsonNumState; | |||||
typedef struct { | |||||
ErlNifEnv* env; | |||||
jiffy_st* atoms; | |||||
ERL_NIF_TERM arg; | |||||
ErlNifBinary bin; | |||||
int has_bignum; | |||||
char* p; | |||||
unsigned char* u; | |||||
int i; | |||||
int len; | |||||
char* st_data; | |||||
int st_size; | |||||
int st_top; | |||||
} Decoder; | |||||
void | |||||
dec_init(Decoder* d, ErlNifEnv* env, ERL_NIF_TERM arg, ErlNifBinary* bin) | |||||
{ | |||||
int i; | |||||
d->env = env; | |||||
d->atoms = enif_priv_data(env); | |||||
d->arg = arg; | |||||
d->has_bignum = 0; | |||||
d->p = (char*) bin->data; | |||||
d->u = bin->data; | |||||
d->len = bin->size; | |||||
d->i = 0; | |||||
d->st_data = (char*) enif_alloc(STACK_SIZE_INC * sizeof(char)); | |||||
d->st_size = STACK_SIZE_INC; | |||||
d->st_top = 0; | |||||
for(i = 0; i < d->st_size; i++) { | |||||
d->st_data[i] = st_invalid; | |||||
} | |||||
d->st_data[0] = st_value; | |||||
d->st_top++; | |||||
} | |||||
void | |||||
dec_destroy(Decoder* d) | |||||
{ | |||||
if(d->st_data != NULL) { | |||||
enif_free(d->st_data); | |||||
} | |||||
} | |||||
ERL_NIF_TERM | |||||
dec_error(Decoder* d, const char* atom) | |||||
{ | |||||
ERL_NIF_TERM pos = enif_make_int(d->env, d->i+1); | |||||
ERL_NIF_TERM msg = make_atom(d->env, atom); | |||||
ERL_NIF_TERM ret = enif_make_tuple2(d->env, pos, msg); | |||||
return enif_make_tuple2(d->env, d->atoms->atom_error, ret); | |||||
} | |||||
char | |||||
dec_curr(Decoder* d) | |||||
{ | |||||
return d->st_data[d->st_top-1]; | |||||
} | |||||
int | |||||
dec_top(Decoder* d) | |||||
{ | |||||
return d->st_top; | |||||
} | |||||
void | |||||
dec_push(Decoder* d, char val) | |||||
{ | |||||
char* tmp; | |||||
int new_sz; | |||||
int i; | |||||
if(d->st_top >= d->st_size) { | |||||
new_sz = d->st_size + STACK_SIZE_INC; | |||||
tmp = (char*) enif_alloc(new_sz * sizeof(char)); | |||||
memcpy(tmp, d->st_data, d->st_size * sizeof(char)); | |||||
enif_free(d->st_data); | |||||
d->st_data = tmp; | |||||
d->st_size = new_sz; | |||||
for(i = d->st_top; i < d->st_size; i++) { | |||||
d->st_data[i] = st_invalid; | |||||
} | |||||
} | |||||
d->st_data[d->st_top++] = val; | |||||
} | |||||
void | |||||
dec_pop(Decoder* d, char val) | |||||
{ | |||||
assert(d->st_data[d->st_top-1] == val && "popped invalid state."); | |||||
d->st_data[d->st_top-1] = st_invalid; | |||||
d->st_top--; | |||||
} | |||||
int | |||||
dec_string(Decoder* d, ERL_NIF_TERM* value) | |||||
{ | |||||
int has_escape = 0; | |||||
int num_escapes = 0; | |||||
int st; | |||||
int ulen; | |||||
int ui; | |||||
int hi; | |||||
int lo; | |||||
char* chrbuf; | |||||
int chrpos; | |||||
if(d->p[d->i] != '\"') { | |||||
return 0; | |||||
} | |||||
d->i++; | |||||
st = d->i; | |||||
while(d->i < d->len) { | |||||
if(d->u[d->i] < 0x20) { | |||||
return 0; | |||||
} else if(d->p[d->i] == '\"') { | |||||
d->i++; | |||||
goto parse; | |||||
} else if(d->p[d->i] == '\\') { | |||||
if(d->i+1 >= d->len) { | |||||
return 0; | |||||
} | |||||
has_escape = 1; | |||||
num_escapes += 1; | |||||
d->i++; | |||||
switch(d->p[d->i]) { | |||||
case '\"': | |||||
case '\\': | |||||
case '/': | |||||
case 'b': | |||||
case 'f': | |||||
case 'n': | |||||
case 'r': | |||||
case 't': | |||||
d->i++; | |||||
break; | |||||
case 'u': | |||||
hi = 0; | |||||
lo = 0; | |||||
d->i++; | |||||
if(d->i + 4 >= d->len) { | |||||
return 0; | |||||
} | |||||
hi = int_from_hex(&(d->u[d->i])); | |||||
d->i += 4; | |||||
if(hi < 0) { | |||||
return 0; | |||||
} | |||||
if(hi >= 0xD800 && hi < 0xDC00) { | |||||
if(d->i + 6 >= d->len) { | |||||
return 0; | |||||
} | |||||
if(d->p[d->i++] != '\\') { | |||||
return 0; | |||||
} else if(d->p[d->i++] != 'u') { | |||||
return 0; | |||||
} | |||||
lo = int_from_hex(&(d->u[d->i])); | |||||
if(lo < 0) { | |||||
return 0; | |||||
} | |||||
hi = utf8_from_pair(hi, lo); | |||||
if(hi < 0) { | |||||
return 0; | |||||
} | |||||
} | |||||
hi = utf8_len(hi); | |||||
if(hi < 0) { | |||||
return 0; | |||||
} | |||||
if(lo == 0) { | |||||
num_escapes += 5 - hi; | |||||
} else { | |||||
num_escapes += 11 - hi; | |||||
} | |||||
break; | |||||
default: | |||||
return 0; | |||||
} | |||||
} else if(d->u[d->i] < 0x80) { | |||||
d->i++; | |||||
} else { | |||||
ulen = -1; | |||||
if((d->u[d->i] & 0xE0) == 0xC0) { | |||||
ulen = 1; | |||||
} else if((d->u[d->i] & 0xF0) == 0xE0) { | |||||
ulen = 2; | |||||
} else if((d->u[d->i] & 0xF8) == 0xF0) { | |||||
ulen = 3; | |||||
} else if((d->u[d->i] & 0xFC) == 0xF8) { | |||||
ulen = 4; | |||||
} else if((d->u[d->i] & 0xFE) == 0xFC) { | |||||
ulen = 5; | |||||
} | |||||
if(ulen < 0) { | |||||
return 0; | |||||
} | |||||
if(d->i + ulen >= d->len) { | |||||
return 0; | |||||
} | |||||
for(ui = 0; ui < ulen; ui++) { | |||||
if((d->u[d->i+1+ui] & 0xC0) != 0x80) { | |||||
return 0; | |||||
} | |||||
} | |||||
// Wikipedia says I have to check that a UTF-8 encoding | |||||
// uses as few bits as possible. This means that we | |||||
// can't do things like encode 't' in three bytes. | |||||
// To check this all we need to ensure is that for each | |||||
// of the following bit patterns that there is at least | |||||
// one 1 bit in each of the x's | |||||
// 11: 110xxxxy 10yyyyyy | |||||
// 16: 1110xxxx 10xyyyyy 10yyyyyy | |||||
// 21: 11110xxx 10xxyyyy 10yyyyyy 10yyyyyy | |||||
// 26: 111110xx 10xxxyyy 10yyyyyy 10yyyyyy 10yyyyyy | |||||
// 31: 1111110x 10xxxxyy 10yyyyyy 10yyyyyy 10yyyyyy 10yyyyyy | |||||
if(ulen == 1) { | |||||
if((d->u[d->i] & 0x1E) == 0) return 0; | |||||
} else if(ulen == 2) { | |||||
if((d->u[d->i] & 0x0F) + (d->u[d->i+1] & 0x20) == 0) return 0; | |||||
} else if(ulen == 3) { | |||||
if((d->u[d->i] & 0x07) + (d->u[d->i+1] & 0x30) == 0) return 0; | |||||
} else if(ulen == 4) { | |||||
if((d->u[d->i] & 0x03) + (d->u[d->i+1] & 0x38) == 0) return 0; | |||||
} else if(ulen == 5) { | |||||
if((d->u[d->i] & 0x01) + (d->u[d->i+1] & 0x3C) == 0) return 0; | |||||
} | |||||
d->i += 1 + ulen; | |||||
} | |||||
} | |||||
parse: | |||||
if(!has_escape) { | |||||
*value = enif_make_sub_binary(d->env, d->arg, st, (d->i - st - 1)); | |||||
return 1; | |||||
} | |||||
hi = 0; | |||||
lo = 0; | |||||
ulen = (d->i - 1) - st - num_escapes; | |||||
chrbuf = (char*) enif_make_new_binary(d->env, ulen, value); | |||||
chrpos = 0; | |||||
ui = st; | |||||
while(ui < d->i - 1) { | |||||
if(d->p[ui] != '\\') { | |||||
chrbuf[chrpos++] = d->p[ui++]; | |||||
continue; | |||||
} | |||||
ui++; | |||||
switch(d->p[ui]) { | |||||
case '\"': | |||||
case '\\': | |||||
case '/': | |||||
chrbuf[chrpos++] = d->p[ui]; | |||||
ui++; | |||||
break; | |||||
case 'b': | |||||
chrbuf[chrpos++] = '\b'; | |||||
ui++; | |||||
break; | |||||
case 'f': | |||||
chrbuf[chrpos++] = '\f'; | |||||
ui++; | |||||
break; | |||||
case 'n': | |||||
chrbuf[chrpos++] = '\n'; | |||||
ui++; | |||||
break; | |||||
case 'r': | |||||
chrbuf[chrpos++] = '\r'; | |||||
ui++; | |||||
break; | |||||
case 't': | |||||
chrbuf[chrpos++] = '\t'; | |||||
ui++; | |||||
break; | |||||
case 'u': | |||||
ui++; | |||||
hi = int_from_hex(&(d->u[ui])); | |||||
if(hi >= 0xD800 && hi < 0xDC00) { | |||||
lo = int_from_hex(&(d->u[ui+6])); | |||||
hi = utf8_from_pair(hi, lo); | |||||
ui += 10; | |||||
} else { | |||||
ui += 4; | |||||
} | |||||
hi = utf8_to_binary(hi, (unsigned char*) chrbuf+chrpos); | |||||
if(hi < 0) { | |||||
return 0; | |||||
} | |||||
chrpos += hi; | |||||
break; | |||||
default: | |||||
return 0; | |||||
} | |||||
} | |||||
return 1; | |||||
} | |||||
int | |||||
dec_number(Decoder* d, ERL_NIF_TERM* value) | |||||
{ | |||||
char state = nst_init; | |||||
char nbuf[NUM_BUF_LEN]; | |||||
int st = d->i; | |||||
int is_double = 0; | |||||
double dval; | |||||
long lval; | |||||
while(d->i < d->len) { | |||||
switch(state) { | |||||
case nst_init: | |||||
switch(d->p[d->i]) { | |||||
case '-': | |||||
state = nst_sign; | |||||
d->i++; | |||||
break; | |||||
case '0': | |||||
state = nst_frac0; | |||||
d->i++; | |||||
break; | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
state = nst_mantissa; | |||||
d->i++; | |||||
break; | |||||
default: | |||||
return 0; | |||||
} | |||||
break; | |||||
case nst_sign: | |||||
switch(d->p[d->i]) { | |||||
case '0': | |||||
state = nst_frac0; | |||||
d->i++; | |||||
break; | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
state = nst_mantissa; | |||||
d->i++; | |||||
break; | |||||
default: | |||||
return 0; | |||||
} | |||||
break; | |||||
case nst_mantissa: | |||||
switch(d->p[d->i]) { | |||||
case '.': | |||||
state = nst_frac1; | |||||
d->i++; | |||||
break; | |||||
case 'e': | |||||
case 'E': | |||||
state = nst_esign; | |||||
d->i++; | |||||
break; | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
d->i++; | |||||
break; | |||||
default: | |||||
goto parse; | |||||
} | |||||
break; | |||||
case nst_frac0: | |||||
switch(d->p[d->i]) { | |||||
case '.': | |||||
state = nst_frac1; | |||||
d->i++; | |||||
break; | |||||
case 'e': | |||||
case 'E': | |||||
state = nst_esign; | |||||
d->i++; | |||||
break; | |||||
default: | |||||
goto parse; | |||||
} | |||||
break; | |||||
case nst_frac1: | |||||
is_double = 1; | |||||
switch(d->p[d->i]) { | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
state = nst_frac; | |||||
d->i++; | |||||
break; | |||||
default: | |||||
goto parse; | |||||
} | |||||
break; | |||||
case nst_frac: | |||||
switch(d->p[d->i]) { | |||||
case 'e': | |||||
case 'E': | |||||
state = nst_esign; | |||||
d->i++; | |||||
break; | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
d->i++; | |||||
break; | |||||
default: | |||||
goto parse; | |||||
} | |||||
break; | |||||
case nst_esign: | |||||
is_double = 1; | |||||
switch(d->p[d->i]) { | |||||
case '-': | |||||
case '+': | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
state = nst_edigit; | |||||
d->i++; | |||||
break; | |||||
default: | |||||
return 0; | |||||
} | |||||
break; | |||||
case nst_edigit: | |||||
switch(d->p[d->i]) { | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
d->i++; | |||||
break; | |||||
default: | |||||
goto parse; | |||||
} | |||||
break; | |||||
default: | |||||
return 0; | |||||
} | |||||
} | |||||
parse: | |||||
switch(state) { | |||||
case nst_init: | |||||
case nst_sign: | |||||
case nst_frac1: | |||||
case nst_esign: | |||||
return 0; | |||||
default: | |||||
break; | |||||
} | |||||
if(st - d->i > NUM_BUF_LEN && is_double) { | |||||
return 0; | |||||
} else if(st - d->i > NUM_BUF_LEN) { | |||||
d->has_bignum = 1; | |||||
*value = enif_make_sub_binary(d->env, d->arg, st, d->i - st); | |||||
*value = enif_make_tuple2(d->env, d->atoms->atom_bignum, *value); | |||||
return 1; | |||||
} | |||||
memset(nbuf, 0, NUM_BUF_LEN); | |||||
memcpy(nbuf, &(d->p[st]), d->i - st); | |||||
errno = 0; | |||||
if(is_double) { | |||||
dval = strtod(nbuf, NULL); | |||||
if(errno == ERANGE) { | |||||
return 0; | |||||
} | |||||
*value = enif_make_double(d->env, dval); | |||||
return 1; | |||||
} | |||||
lval = strtol(nbuf, NULL, 10); | |||||
if(errno == ERANGE) { | |||||
d->has_bignum = 1; | |||||
*value = enif_make_sub_binary(d->env, d->arg, st, d->i - st); | |||||
*value = enif_make_tuple2(d->env, d->atoms->atom_bignum, *value); | |||||
} else { | |||||
*value = enif_make_int64(d->env, lval); | |||||
} | |||||
return 1; | |||||
} | |||||
ERL_NIF_TERM | |||||
make_object(ErlNifEnv* env, ERL_NIF_TERM pairs) | |||||
{ | |||||
ERL_NIF_TERM ret = enif_make_list(env, 0); | |||||
ERL_NIF_TERM key, val; | |||||
while(enif_get_list_cell(env, pairs, &val, &pairs)) { | |||||
if(!enif_get_list_cell(env, pairs, &key, &pairs)) { | |||||
assert(0 == 1 && "Unbalanced object pairs."); | |||||
} | |||||
val = enif_make_tuple2(env, key, val); | |||||
ret = enif_make_list_cell(env, val, ret); | |||||
} | |||||
return enif_make_tuple1(env, ret); | |||||
} | |||||
ERL_NIF_TERM | |||||
make_array(ErlNifEnv* env, ERL_NIF_TERM list) | |||||
{ | |||||
ERL_NIF_TERM ret = enif_make_list(env, 0); | |||||
ERL_NIF_TERM item; | |||||
while(enif_get_list_cell(env, list, &item, &list)) { | |||||
ret = enif_make_list_cell(env, item, ret); | |||||
} | |||||
return ret; | |||||
} | |||||
ERL_NIF_TERM | |||||
decode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) | |||||
{ | |||||
Decoder dec; | |||||
Decoder* d = &dec; | |||||
ErlNifBinary bin; | |||||
ERL_NIF_TERM objs = enif_make_list(env, 0); | |||||
ERL_NIF_TERM curr; | |||||
ERL_NIF_TERM val; | |||||
ERL_NIF_TERM ret; | |||||
if(argc != 1) { | |||||
return enif_make_badarg(env); | |||||
} else if(!enif_inspect_binary(env, argv[0], &bin)) { | |||||
return enif_make_badarg(env); | |||||
} | |||||
dec_init(d, env, argv[0], &bin); | |||||
//fprintf(stderr, "Parsing:\r\n"); | |||||
while(d->i < bin.size) { | |||||
//fprintf(stderr, "state: %d\r\n", dec_curr(d)); | |||||
switch(dec_curr(d)) { | |||||
case st_value: | |||||
switch(d->p[d->i]) { | |||||
case ' ': | |||||
case '\n': | |||||
case '\r': | |||||
case '\t': | |||||
d->i++; | |||||
break; | |||||
case 'n': | |||||
if(d->i + 3 >= d->len) { | |||||
ret = dec_error(d, "invalid_literal"); | |||||
goto done; | |||||
} | |||||
if(memcmp(&(d->p[d->i]), "null", 4) != 0) { | |||||
ret = dec_error(d, "invalid_literal"); | |||||
goto done; | |||||
} | |||||
val = d->atoms->atom_null; | |||||
dec_pop(d, st_value); | |||||
d->i += 4; | |||||
break; | |||||
case 't': | |||||
if(d->i + 3 >= d->len) { | |||||
ret = dec_error(d, "invalid_literal"); | |||||
goto done; | |||||
} | |||||
if(memcmp(&(d->p[d->i]), "true", 4) != 0) { | |||||
ret = dec_error(d, "invalid_literal"); | |||||
goto done; | |||||
} | |||||
val = d->atoms->atom_true; | |||||
dec_pop(d, st_value); | |||||
d->i += 4; | |||||
break; | |||||
case 'f': | |||||
if(d->i + 4 >= bin.size) { | |||||
ret = dec_error(d, "invalid_literal"); | |||||
goto done; | |||||
} | |||||
if(memcmp(&(d->p[d->i]), "false", 5) != 0) { | |||||
ret = dec_error(d, "invalid_literal"); | |||||
goto done; | |||||
} | |||||
val = d->atoms->atom_false; | |||||
dec_pop(d, st_value); | |||||
d->i += 5; | |||||
break; | |||||
case '\"': | |||||
if(!dec_string(d, &val)) { | |||||
ret = dec_error(d, "invalid_string"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_value); | |||||
break; | |||||
case '-': | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
if(!dec_number(d, &val)) { | |||||
ret = dec_error(d, "invalid_number"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_value); | |||||
break; | |||||
case '{': | |||||
dec_push(d, st_object); | |||||
dec_push(d, st_key); | |||||
objs = enif_make_list_cell(env, curr, objs); | |||||
curr = enif_make_list(env, 0); | |||||
d->i++; | |||||
break; | |||||
case '[': | |||||
dec_push(d, st_array); | |||||
dec_push(d, st_value); | |||||
objs = enif_make_list_cell(env, curr, objs); | |||||
curr = enif_make_list(env, 0); | |||||
d->i++; | |||||
break; | |||||
case ']': | |||||
if(!enif_is_empty_list(env, curr)) { | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_value); | |||||
if(dec_curr(d) != st_array) { | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_array); | |||||
dec_pop(d, st_value); | |||||
val = curr; // curr is [] | |||||
if(!enif_get_list_cell(env, objs, &curr, &objs)) { | |||||
ret = dec_error(d, "internal_error"); | |||||
goto done; | |||||
} | |||||
d->i++; | |||||
break; | |||||
default: | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
if(dec_top(d) == 0) { | |||||
dec_push(d, st_done); | |||||
} else if(dec_curr(d) != st_value && dec_curr(d) != st_key) { | |||||
dec_push(d, st_comma); | |||||
curr = enif_make_list_cell(env, val, curr); | |||||
} | |||||
break; | |||||
case st_key: | |||||
switch(d->p[d->i]) { | |||||
case ' ': | |||||
case '\n': | |||||
case '\r': | |||||
case '\t': | |||||
d->i++; | |||||
break; | |||||
case '\"': | |||||
if(!dec_string(d, &val)) { | |||||
ret = dec_error(d, "invalid_string"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_key); | |||||
dec_push(d, st_colon); | |||||
curr = enif_make_list_cell(env, val, curr); | |||||
break; | |||||
case '}': | |||||
if(!enif_is_empty_list(env, curr)) { | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_key); | |||||
dec_pop(d, st_object); | |||||
dec_pop(d, st_value); | |||||
val = enif_make_tuple1(env, curr); | |||||
if(!enif_get_list_cell(env, objs, &curr, &objs)) { | |||||
ret = dec_error(d, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(dec_top(d) == 0) { | |||||
dec_push(d, st_done); | |||||
} else { | |||||
dec_push(d, st_comma); | |||||
curr = enif_make_list_cell(env, val, curr); | |||||
} | |||||
d->i++; | |||||
break; | |||||
default: | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
break; | |||||
case st_colon: | |||||
switch(d->p[d->i]) { | |||||
case ' ': | |||||
case '\n': | |||||
case '\r': | |||||
case '\t': | |||||
d->i++; | |||||
break; | |||||
case ':': | |||||
dec_pop(d, st_colon); | |||||
dec_push(d, st_value); | |||||
d->i++; | |||||
break; | |||||
default: | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
break; | |||||
case st_comma: | |||||
switch(d->p[d->i]) { | |||||
case ' ': | |||||
case '\n': | |||||
case '\r': | |||||
case '\t': | |||||
d->i++; | |||||
break; | |||||
case ',': | |||||
dec_pop(d, st_comma); | |||||
switch(dec_curr(d)) { | |||||
case st_object: | |||||
dec_push(d, st_key); | |||||
break; | |||||
case st_array: | |||||
dec_push(d, st_value); | |||||
break; | |||||
default: | |||||
ret = dec_error(d, "internal_error"); | |||||
goto done; | |||||
} | |||||
d->i++; | |||||
break; | |||||
case '}': | |||||
dec_pop(d, st_comma); | |||||
if(dec_curr(d) != st_object) { | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_object); | |||||
dec_pop(d, st_value); | |||||
val = make_object(env, curr); | |||||
if(!enif_get_list_cell(env, objs, &curr, &objs)) { | |||||
ret = dec_error(d, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(dec_top(d) > 0) { | |||||
dec_push(d, st_comma); | |||||
curr = enif_make_list_cell(env, val, curr); | |||||
} else { | |||||
dec_push(d, st_done); | |||||
} | |||||
d->i++; | |||||
break; | |||||
case ']': | |||||
dec_pop(d, st_comma); | |||||
if(dec_curr(d) != st_array) { | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
dec_pop(d, st_array); | |||||
dec_pop(d, st_value); | |||||
val = make_array(env, curr); | |||||
if(!enif_get_list_cell(env, objs, &curr, &objs)) { | |||||
ret = dec_error(d, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(dec_top(d) > 0) { | |||||
dec_push(d, st_comma); | |||||
curr = enif_make_list_cell(env, val, curr); | |||||
} else { | |||||
dec_push(d, st_done); | |||||
} | |||||
d->i++; | |||||
break; | |||||
default: | |||||
ret = dec_error(d, "invalid_json"); | |||||
goto done; | |||||
} | |||||
break; | |||||
case st_done: | |||||
switch(d->p[d->i]) { | |||||
case ' ': | |||||
case '\n': | |||||
case '\r': | |||||
case '\t': | |||||
d->i++; | |||||
break; | |||||
default: | |||||
ret = dec_error(d, "invalid_trailing_data"); | |||||
goto done; | |||||
} | |||||
break; | |||||
default: | |||||
ret = dec_error(d, "invalid_internal_state"); | |||||
goto done; | |||||
} | |||||
} | |||||
if(dec_curr(d) != st_done) { | |||||
ret = dec_error(d, "truncated_json"); | |||||
} else if(d->has_bignum) { | |||||
ret = enif_make_tuple2(env, d->atoms->atom_bignum, val); | |||||
} else { | |||||
ret = enif_make_tuple2(env, d->atoms->atom_ok, val); | |||||
} | |||||
done: | |||||
dec_destroy(d); | |||||
return ret; | |||||
} |
@ -0,0 +1,556 @@ | |||||
#include <assert.h> | |||||
#include <stdio.h> | |||||
#include <string.h> | |||||
#include "erl_nif.h" | |||||
#include "jiffy.h" | |||||
#define BIN_INC_SIZE 1024 | |||||
typedef struct { | |||||
ErlNifEnv* env; | |||||
jiffy_st* atoms; | |||||
int count; | |||||
ERL_NIF_TERM iolist; | |||||
ErlNifBinary curr; | |||||
int cleared; | |||||
char* p; | |||||
unsigned char* u; | |||||
size_t i; | |||||
} Encoder; | |||||
int | |||||
enc_init(Encoder* e, ErlNifEnv* env) | |||||
{ | |||||
e->env = env; | |||||
e->atoms = enif_priv_data(env); | |||||
e->count = 0; | |||||
e->iolist = enif_make_list(env, 0); | |||||
if(!enif_alloc_binary(BIN_INC_SIZE, &(e->curr))) { | |||||
return 0; | |||||
} | |||||
e->cleared = 0; | |||||
e->p = (char*) e->curr.data; | |||||
e->u = (unsigned char*) e->curr.data; | |||||
e->i = 0; | |||||
return 1; | |||||
} | |||||
void | |||||
enc_destroy(Encoder* e) | |||||
{ | |||||
if(!e->cleared) { | |||||
enif_release_binary(&(e->curr)); | |||||
} | |||||
} | |||||
ERL_NIF_TERM | |||||
enc_error(Encoder* e, const char* msg) | |||||
{ | |||||
assert(0 && msg); | |||||
return make_error(e->atoms, e->env, msg); | |||||
} | |||||
int | |||||
enc_result(Encoder* e, ERL_NIF_TERM* value) | |||||
{ | |||||
if(e->i != e->curr.size) { | |||||
if(!enif_realloc_binary(&(e->curr), e->i)) { | |||||
return 0; | |||||
} | |||||
} | |||||
*value = enif_make_binary(e->env, &(e->curr)); | |||||
e->cleared = 1; | |||||
return 1; | |||||
} | |||||
int | |||||
enc_ensure(Encoder* e, size_t req) | |||||
{ | |||||
size_t new_sz; | |||||
if(req < e->curr.size - e->i) { | |||||
return 1; | |||||
} | |||||
new_sz = req - (e->curr.size - e->i); | |||||
new_sz += BIN_INC_SIZE - (new_sz % BIN_INC_SIZE); | |||||
assert(new_sz % BIN_INC_SIZE == 0 && "Invalid modulo math."); | |||||
if(!enif_realloc_binary(&(e->curr), new_sz)) { | |||||
return 0; | |||||
} | |||||
memset(&(e->u[e->i]), 0, e->curr.size - e->i); | |||||
return 1; | |||||
} | |||||
int | |||||
enc_literal(Encoder* e, const char* literal, size_t len) | |||||
{ | |||||
if(!enc_ensure(e, len)) { | |||||
return 0; | |||||
} | |||||
memcpy(&(e->p[e->i]), literal, len); | |||||
e->i += len; | |||||
e->count++; | |||||
return 1; | |||||
} | |||||
int | |||||
enc_string(Encoder* e, ERL_NIF_TERM val) | |||||
{ | |||||
ErlNifBinary bin; | |||||
char atom[512]; | |||||
int esc_extra = 0; | |||||
int ulen; | |||||
int ui; | |||||
int i; | |||||
if(enif_is_binary(e->env, val)) { | |||||
if(!enif_inspect_binary(e->env, val, &bin)) { | |||||
return 0; | |||||
} | |||||
} else if(enif_is_atom(e->env, val)) { | |||||
if(!enif_get_atom(e->env, val, atom, 512, ERL_NIF_LATIN1)) { | |||||
return 0; | |||||
} | |||||
// Fake as a binary for code below. | |||||
bin.data = (unsigned char*) atom; | |||||
bin.size = strlen(atom); | |||||
} else { | |||||
return 0; | |||||
} | |||||
i = 0; | |||||
while(i < bin.size) { | |||||
switch((char) bin.data[i]) { | |||||
case '\"': | |||||
case '\\': | |||||
case '/': | |||||
case '\b': | |||||
case '\f': | |||||
case '\n': | |||||
case '\r': | |||||
case '\t': | |||||
esc_extra += 1; | |||||
i++; | |||||
continue; | |||||
default: | |||||
if(bin.data[i] < 0x20) { | |||||
esc_extra += 5; | |||||
i++; | |||||
continue; | |||||
} else if(bin.data[i] < 0x80) { | |||||
i++; | |||||
continue; | |||||
} | |||||
ulen = -1; | |||||
if((bin.data[i] & 0xE0) == 0xC0) { | |||||
ulen = 1; | |||||
} else if((bin.data[i] & 0xF0) == 0xE0) { | |||||
ulen = 2; | |||||
} else if((bin.data[i] & 0xF8) == 0xF0) { | |||||
ulen = 3; | |||||
} else if((bin.data[i] & 0xFC) == 0xF8) { | |||||
ulen = 4; | |||||
} else if((bin.data[i] & 0xFE) == 0xFC) { | |||||
ulen = 5; | |||||
} | |||||
if(ulen < 0) { | |||||
return 0; | |||||
} | |||||
if(i+1+ulen > bin.size) { | |||||
return 0; | |||||
} | |||||
for(ui = 0; ui < ulen; ui++) { | |||||
if((bin.data[i+1+ui] & 0xC0) != 0x80) { | |||||
return 0; | |||||
} | |||||
} | |||||
if(ulen == 1) { | |||||
if((bin.data[i] & 0x1E) == 0) | |||||
return 0; | |||||
} else if(ulen == 2) { | |||||
if((bin.data[i] & 0x0F) + (bin.data[i+1] & 0x20) == 0) | |||||
return 0; | |||||
} else if(ulen == 3) { | |||||
if((bin.data[i] & 0x07) + (bin.data[i+1] & 0x30) == 0) | |||||
return 0; | |||||
} else if(ulen == 4) { | |||||
if((bin.data[i] & 0x03) + (bin.data[i+1] & 0x38) == 0) | |||||
return 0; | |||||
} else if(ulen == 5) { | |||||
if((bin.data[i] & 0x01) + (bin.data[i+1] & 0x3C) == 0) | |||||
return 0; | |||||
} | |||||
i += 1 + ulen; | |||||
} | |||||
} | |||||
if(!enc_ensure(e, bin.size + esc_extra + 2)) { | |||||
return 0; | |||||
} | |||||
e->p[e->i++] = '\"'; | |||||
i = 0; | |||||
while(i < bin.size) { | |||||
switch((char) bin.data[i]) { | |||||
case '\"': | |||||
case '\\': | |||||
case '/': | |||||
e->p[e->i++] = '\\'; | |||||
e->u[e->i++] = bin.data[i]; | |||||
i++; | |||||
continue; | |||||
case '\b': | |||||
e->p[e->i++] = '\\'; | |||||
e->p[e->i++] = 'b'; | |||||
i++; | |||||
continue; | |||||
case '\f': | |||||
e->p[e->i++] = '\\'; | |||||
e->p[e->i++] = 'f'; | |||||
i++; | |||||
continue; | |||||
case '\n': | |||||
e->p[e->i++] = '\\'; | |||||
e->p[e->i++] = 'n'; | |||||
i++; | |||||
continue; | |||||
case '\r': | |||||
e->p[e->i++] = '\\'; | |||||
e->p[e->i++] = 'r'; | |||||
i++; | |||||
continue; | |||||
case '\t': | |||||
e->p[e->i++] = '\\'; | |||||
e->p[e->i++] = 't'; | |||||
i++; | |||||
continue; | |||||
default: | |||||
if(bin.data[i] < 0x20) { | |||||
e->p[e->i++] = '\\'; | |||||
e->p[e->i++] = 'u'; | |||||
if(!int_to_hex(bin.data[i], &(e->p[e->i]))) { | |||||
return 0; | |||||
} | |||||
e->i += 4; | |||||
i++; | |||||
} else { | |||||
e->u[e->i++] = bin.data[i++]; | |||||
} | |||||
} | |||||
} | |||||
e->p[e->i++] = '\"'; | |||||
e->count++; | |||||
return 1; | |||||
} | |||||
int | |||||
enc_long(Encoder* e, long val) | |||||
{ | |||||
if(!enc_ensure(e, 32)) { | |||||
return 0; | |||||
} | |||||
snprintf(&(e->p[e->i]), 32, "%ld", val); | |||||
e->i += strlen(&(e->p[e->i])); | |||||
e->count++; | |||||
return 1; | |||||
} | |||||
int | |||||
enc_double(Encoder* e, double val) | |||||
{ | |||||
if(!enc_ensure(e, 32)) { | |||||
return 0; | |||||
} | |||||
snprintf(&(e->p[e->i]), 32, "%g", val); | |||||
e->i += strlen(&(e->p[e->i])); | |||||
e->count++; | |||||
return 1; | |||||
} | |||||
int | |||||
enc_char(Encoder* e, char c) | |||||
{ | |||||
if(!enc_ensure(e, 1)) { | |||||
return 0; | |||||
} | |||||
e->p[e->i++] = c; | |||||
return 1; | |||||
} | |||||
int | |||||
enc_start_object(Encoder* e) | |||||
{ | |||||
e->count++; | |||||
return enc_char(e, '{'); | |||||
} | |||||
int | |||||
enc_end_object(Encoder* e) | |||||
{ | |||||
return enc_char(e, '}'); | |||||
} | |||||
int | |||||
enc_start_array(Encoder* e) | |||||
{ | |||||
e->count++; | |||||
return enc_char(e, '['); | |||||
} | |||||
int | |||||
enc_end_array(Encoder* e) | |||||
{ | |||||
return enc_char(e, ']'); | |||||
} | |||||
int | |||||
enc_colon(Encoder* e) | |||||
{ | |||||
return enc_char(e, ':'); | |||||
} | |||||
int | |||||
enc_comma(Encoder* e) | |||||
{ | |||||
return enc_char(e, ','); | |||||
} | |||||
ERL_NIF_TERM | |||||
encode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) | |||||
{ | |||||
Encoder enc; | |||||
Encoder* e = &enc; | |||||
ERL_NIF_TERM ret; | |||||
ERL_NIF_TERM stack; | |||||
ERL_NIF_TERM curr; | |||||
ERL_NIF_TERM item; | |||||
const ERL_NIF_TERM* tuple; | |||||
int arity; | |||||
double dval; | |||||
long lval; | |||||
int has_unknown = 0; | |||||
if(argc != 1) { | |||||
return enif_make_badarg(env); | |||||
} | |||||
if(!enc_init(e, env)) { | |||||
return enif_make_badarg(env); | |||||
} | |||||
stack = enif_make_list1(env, argv[0]); | |||||
while(!enif_is_empty_list(env, stack)) { | |||||
if(!enif_get_list_cell(env, stack, &curr, &stack)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(enif_is_identical(curr, e->atoms->ref_object)) { | |||||
if(!enif_get_list_cell(env, stack, &curr, &stack)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(enif_is_empty_list(env, curr)) { | |||||
if(!enc_end_object(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
continue; | |||||
} | |||||
if(!enif_get_list_cell(env, curr, &item, &curr)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(!enif_get_tuple(env, item, &arity, &tuple)) { | |||||
ret = enc_error(e, "invalid_object_pair"); | |||||
goto done; | |||||
} | |||||
if(arity != 2) { | |||||
ret = enc_error(e, "invalid_object_pair"); | |||||
goto done; | |||||
} | |||||
if(!enc_comma(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(!enc_string(e, tuple[0])) { | |||||
ret = enc_error(e, "invalid_object_key"); | |||||
goto done; | |||||
} | |||||
if(!enc_colon(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
stack = enif_make_list_cell(env, curr, stack); | |||||
stack = enif_make_list_cell(env, e->atoms->ref_object, stack); | |||||
stack = enif_make_list_cell(env, tuple[1], stack); | |||||
} else if(enif_is_identical(curr, e->atoms->ref_array)) { | |||||
if(!enif_get_list_cell(env, stack, &curr, &stack)) { | |||||
ret = enc_error(e, "internal_error.5"); | |||||
goto done; | |||||
} | |||||
if(enif_is_empty_list(env, curr)) { | |||||
if(!enc_end_array(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
continue; | |||||
} | |||||
if(!enc_comma(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(!enif_get_list_cell(env, curr, &item, &curr)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
stack = enif_make_list_cell(env, curr, stack); | |||||
stack = enif_make_list_cell(env, e->atoms->ref_array, stack); | |||||
stack = enif_make_list_cell(env, item, stack); | |||||
} else if(enif_compare(curr, e->atoms->atom_null) == 0) { | |||||
if(!enc_literal(e, "null", 4)) { | |||||
ret = enc_error(e, "null"); | |||||
goto done; | |||||
} | |||||
} else if(enif_compare(curr, e->atoms->atom_true) == 0) { | |||||
if(!enc_literal(e, "true", 4)) { | |||||
ret = enc_error(e, "true"); | |||||
goto done; | |||||
} | |||||
} else if(enif_compare(curr, e->atoms->atom_false) == 0) { | |||||
if(!enc_literal(e, "false", 5)) { | |||||
ret = enc_error(e, "false"); | |||||
goto done; | |||||
} | |||||
} else if(enif_is_binary(env, curr)) { | |||||
if(!enc_string(e, curr)) { | |||||
ret = enc_error(e, "invalid_string"); | |||||
goto done; | |||||
} | |||||
} else if(enif_is_atom(env, curr)) { | |||||
if(!enc_string(e, curr)) { | |||||
ret = enc_error(e, "invalid_string"); | |||||
goto done; | |||||
} | |||||
} else if(enif_get_int64(env, curr, &lval)) { | |||||
if(!enc_long(e, lval)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
} else if(enif_get_double(env, curr, &dval)) { | |||||
if(!enc_double(e, dval)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
} else if(enif_get_tuple(env, curr, &arity, &tuple)) { | |||||
if(arity != 1) { | |||||
ret = enc_error(e, "invalid_ejson"); | |||||
goto done; | |||||
} | |||||
if(!enif_is_list(env, tuple[0])) { | |||||
ret = enc_error(e, "invalid_object"); | |||||
goto done; | |||||
} | |||||
if(!enc_start_object(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(enif_is_empty_list(env, tuple[0])) { | |||||
if(!enc_end_object(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
continue; | |||||
} | |||||
if(!enif_get_list_cell(env, tuple[0], &item, &curr)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(!enif_get_tuple(env, item, &arity, &tuple)) { | |||||
ret = enc_error(e, "invalid_object_pair"); | |||||
goto done; | |||||
} | |||||
if(arity != 2) { | |||||
ret = enc_error(e, "invalid_object_pair"); | |||||
goto done; | |||||
} | |||||
if(!enc_string(e, tuple[0])) { | |||||
ret = enc_error(e, "invalid_object_key"); | |||||
goto done; | |||||
} | |||||
if(!enc_colon(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
stack = enif_make_list_cell(env, curr, stack); | |||||
stack = enif_make_list_cell(env, e->atoms->ref_object, stack); | |||||
stack = enif_make_list_cell(env, tuple[1], stack); | |||||
} else if(enif_is_list(env, curr)) { | |||||
if(!enc_start_array(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
if(enif_is_empty_list(env, curr)) { | |||||
if(!enc_end_array(e)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
continue; | |||||
} | |||||
if(!enif_get_list_cell(env, curr, &item, &curr)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
stack = enif_make_list_cell(env, curr, stack); | |||||
stack = enif_make_list_cell(env, e->atoms->ref_array, stack); | |||||
stack = enif_make_list_cell(env, item, stack); | |||||
} else { | |||||
has_unknown = 1; | |||||
ret = enc_error(e, "invalid_ejson"); | |||||
goto done; | |||||
/* | |||||
if(!enc_unknown(env, curr)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
*/ | |||||
} | |||||
} while(!enif_is_empty_list(env, stack)); | |||||
if(!enc_result(e, &item)) { | |||||
ret = enc_error(e, "internal_error"); | |||||
goto done; | |||||
} | |||||
ret = enif_make_tuple2(env, e->atoms->atom_ok, item); | |||||
done: | |||||
enc_destroy(e); | |||||
return ret; | |||||
} |
@ -0,0 +1,52 @@ | |||||
#include "jiffy.h" | |||||
static int | |||||
load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) | |||||
{ | |||||
jiffy_st* st = enif_alloc(sizeof(jiffy_st)); | |||||
if(st == NULL) { | |||||
return 1; | |||||
} | |||||
st->atom_ok = make_atom(env, "ok"); | |||||
st->atom_error = make_atom(env, "error"); | |||||
st->atom_null = make_atom(env, "null"); | |||||
st->atom_true = make_atom(env, "true"); | |||||
st->atom_false = make_atom(env, "false"); | |||||
st->atom_bignum = make_atom(env, "bignum"); | |||||
st->ref_object = enif_make_ref(env); | |||||
st->ref_array = enif_make_ref(env); | |||||
*priv = (void*) st; | |||||
return 0; | |||||
} | |||||
static int | |||||
reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) | |||||
{ | |||||
return 0; | |||||
} | |||||
static int | |||||
upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM info) | |||||
{ | |||||
*priv = *old_priv; | |||||
return 0; | |||||
} | |||||
static void | |||||
unload(ErlNifEnv* env, void* priv) | |||||
{ | |||||
enif_free(priv); | |||||
return; | |||||
} | |||||
static ErlNifFunc funcs[] = | |||||
{ | |||||
{"nif_decode", 1, decode}, | |||||
{"nif_encode", 1, encode} | |||||
}; | |||||
ERL_NIF_INIT(jiffy, funcs, &load, &reload, &upgrade, &unload); |
@ -0,0 +1,30 @@ | |||||
#ifndef JIFFY_H | |||||
#define JIFFY_H | |||||
#include "erl_nif.h" | |||||
typedef struct { | |||||
ERL_NIF_TERM atom_ok; | |||||
ERL_NIF_TERM atom_error; | |||||
ERL_NIF_TERM atom_null; | |||||
ERL_NIF_TERM atom_true; | |||||
ERL_NIF_TERM atom_false; | |||||
ERL_NIF_TERM atom_bignum; | |||||
ERL_NIF_TERM ref_object; | |||||
ERL_NIF_TERM ref_array; | |||||
} jiffy_st; | |||||
ERL_NIF_TERM make_atom(ErlNifEnv* env, const char* name); | |||||
ERL_NIF_TERM make_ok(jiffy_st* st, ErlNifEnv* env, ERL_NIF_TERM data); | |||||
ERL_NIF_TERM make_error(jiffy_st* st, ErlNifEnv* env, const char* error); | |||||
ERL_NIF_TERM decode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); | |||||
ERL_NIF_TERM encode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); | |||||
int int_from_hex(const unsigned char* p); | |||||
int int_to_hex(int val, char* p); | |||||
int utf8_len(int c); | |||||
int utf8_from_pair(int hi, int lo); | |||||
int utf8_to_binary(int c, unsigned char* buf); | |||||
#endif // Included JIFFY_H |
@ -0,0 +1,128 @@ | |||||
static const char hexvals[256] = { | |||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, | |||||
-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |||||
-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 | |||||
}; | |||||
static const char hexdigits[16] = { | |||||
'0', '1', '2', '3', | |||||
'4', '5', '6', '7', | |||||
'8', '9', 'A', 'B', | |||||
'C', 'D', 'E', 'F' | |||||
}; | |||||
int | |||||
int_from_hex(const unsigned char* p) | |||||
{ | |||||
unsigned char* h = (unsigned char*) p; | |||||
int ret; | |||||
if(hexvals[*(h+0)] < 0) return -1; | |||||
if(hexvals[*(h+1)] < 0) return -1; | |||||
if(hexvals[*(h+2)] < 0) return -1; | |||||
if(hexvals[*(h+3)] < 0) return -1; | |||||
ret = (hexvals[*(h+0)] << 12) | |||||
+ (hexvals[*(h+1)] << 8) | |||||
+ (hexvals[*(h+2)] << 4) | |||||
+ (hexvals[*(h+3)] << 0); | |||||
return ret; | |||||
} | |||||
int | |||||
int_to_hex(int val, char* p) | |||||
{ | |||||
if(val < 0 || val > 65535) | |||||
return 0; | |||||
p[0] = hexdigits[(val >> 12) & 0xF]; | |||||
p[1] = hexdigits[(val >> 8) & 0xF]; | |||||
p[2] = hexdigits[(val >> 4) & 0xF]; | |||||
p[3] = hexdigits[val & 0xF]; | |||||
return 1; | |||||
} | |||||
int | |||||
utf8_len(int c) | |||||
{ | |||||
if(c < 128) { | |||||
return 1; | |||||
} else if(c < 0x800) { | |||||
return 2; | |||||
} else if(c < 0x10000) { | |||||
if(c < 0xD800 || (c > 0xDFFF && c < 0xFFFE)) { | |||||
return 3; | |||||
} else { | |||||
return -1; | |||||
} | |||||
} else if(c < 0x200000) { | |||||
return 4; | |||||
} else if(c < 0x4000000) { | |||||
return 5; | |||||
} else if(c < 0x80000000) { | |||||
return 6; | |||||
} else { | |||||
return -1; | |||||
} | |||||
} | |||||
int | |||||
utf8_from_pair(int hi, int lo) | |||||
{ | |||||
if(hi < 0xD800 || hi >= 0xDC00) return -1; | |||||
if(lo < 0xDC00 || lo > 0xDFFF) return -1; | |||||
return ((hi & 0x3FF) << 10) + (lo & 0x3FF) + 0x10000; | |||||
} | |||||
int | |||||
utf8_to_binary(int c, unsigned char* buf) | |||||
{ | |||||
if(c < 0x80) { | |||||
buf[0] = (unsigned char) c; | |||||
return 1; | |||||
} else if(c < 0x800) { | |||||
buf[0] = (unsigned char) 0xC0 + (c >> 6); | |||||
buf[1] = (unsigned char) 0x80 + (c & 0x3F); | |||||
return 2; | |||||
} else if(c < 0x10000) { | |||||
if(c < 0xD800 || (c > 0xDFFF && c < 0xFFFE)) { | |||||
buf[0] = (unsigned char) 0xE0 + (c >> 12); | |||||
buf[1] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); | |||||
buf[2] = (unsigned char) 0x80 + (c & 0x3F); | |||||
return 3; | |||||
} else { | |||||
return -1; | |||||
} | |||||
} else if(c < 0x200000) { | |||||
buf[0] = (unsigned char) 0xF0 + (c >> 18); | |||||
buf[1] = (unsigned char) 0x80 + ((c >> 12) & 0x3F); | |||||
buf[2] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); | |||||
buf[3] = (unsigned char) 0x80 + (c & 0x3F); | |||||
return 4; | |||||
} else if(c < 0x4000000) { | |||||
buf[0] = (unsigned char) 0xF8 + (c >> 24); | |||||
buf[1] = (unsigned char) 0x80 + ((c >> 18) & 0x3F); | |||||
buf[2] = (unsigned char) 0x80 + ((c >> 12) & 0x3F); | |||||
buf[3] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); | |||||
buf[4] = (unsigned char) 0x80 + (c & 0x3F); | |||||
return 5; | |||||
} else if(c < 0x80000000) { | |||||
buf[0] = (unsigned char) 0xFC + (c >> 30); | |||||
buf[1] = (unsigned char) 0x80 + ((c >> 24) & 0x3F); | |||||
buf[2] = (unsigned char) 0x80 + ((c >> 18) & 0x3F); | |||||
buf[3] = (unsigned char) 0x80 + ((c >> 12) & 0x3F); | |||||
buf[4] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); | |||||
buf[5] = (unsigned char) 0x80 + (c & 0x3F); | |||||
return 6; | |||||
} | |||||
return -1; | |||||
} | |||||
@ -0,0 +1,23 @@ | |||||
#include "jiffy.h" | |||||
ERL_NIF_TERM | |||||
make_atom(ErlNifEnv* env, const char* name) | |||||
{ | |||||
ERL_NIF_TERM ret; | |||||
if(enif_make_existing_atom(env, name, &ret, ERL_NIF_LATIN1)) { | |||||
return ret; | |||||
} | |||||
return enif_make_atom(env, name); | |||||
} | |||||
ERL_NIF_TERM | |||||
make_ok(jiffy_st* st, ErlNifEnv* env, ERL_NIF_TERM value) | |||||
{ | |||||
return enif_make_tuple2(env, st->atom_ok, value); | |||||
} | |||||
ERL_NIF_TERM | |||||
make_error(jiffy_st* st, ErlNifEnv* env, const char* error) | |||||
{ | |||||
return enif_make_tuple2(env, st->atom_error, make_atom(env, error)); | |||||
} |
@ -0,0 +1,17 @@ | |||||
{port_sources, ["c_src/*.c"]}. | |||||
{so_name, "jiffy.so"}. | |||||
{port_envs, [ | |||||
%% Link the spidermonkey library | |||||
{".*", "CFLAGS", "$CFLAGS -g -Wall"}, | |||||
%% Make sure to link -lstdc++ on linux or solaris | |||||
{"(linux|solaris)", "LDFLAGS", "$LDFLAGS -lstdc++"}, | |||||
%% OS X Leopard flags for 64-bit | |||||
{"darwin9.*-64$", "CXXFLAGS", "-m64"}, | |||||
{"darwin9.*-64$", "LDFLAGS", "-arch x86_64"}, | |||||
%% OS X Snow Leopard flags for 32-bit | |||||
{"darwin10.*-32$", "CXXFLAGS", "-m32"}, | |||||
{"darwin10.*-32$", "LDFLAGS", "-arch i386"} | |||||
]}. |
@ -0,0 +1,7 @@ | |||||
{application, jiffy, [ | |||||
{description, "JSON Decoder/Encoder."}, | |||||
{vsn, "%VSN%"}, | |||||
{registered, []}, | |||||
{mod, reloading, []}, | |||||
{applications, [kernel]} | |||||
]}. |
@ -0,0 +1,62 @@ | |||||
-module(jiffy). | |||||
-export([decode/1, encode/1]). | |||||
-define(NOT_LOADED, not_loaded(?LINE)). | |||||
-on_load(init/0). | |||||
decode(Data) -> | |||||
case nif_decode(Data) of | |||||
{bignum, EJson} -> | |||||
{ok, debignum(EJson)}; | |||||
Else -> | |||||
Else | |||||
end. | |||||
encode(Data) -> | |||||
nif_encode(Data). | |||||
nif_decode(_Data) -> | |||||
?NOT_LOADED. | |||||
nif_encode(_Data) -> | |||||
?NOT_LOADED. | |||||
debignum({bignum, Value}) -> | |||||
list_to_integer(binary_to_list(Value)); | |||||
debignum({Pairs}) when is_list(Pairs) -> | |||||
debignum_obj(Pairs, []); | |||||
debignum(Vals) when is_list(Vals) -> | |||||
debignum_arr(Vals, []); | |||||
debignum(Val) -> | |||||
Val. | |||||
debignum_obj([], Acc) -> | |||||
{lists:reverse(Acc)}; | |||||
debignum_obj([{K, V} | Pairs], Acc) -> | |||||
debignum_obj(Pairs, [{K, debignum(V)} | Acc]). | |||||
debignum_arr([], Acc) -> | |||||
lists:reverse(Acc); | |||||
debignum_arr([V | Vals], Acc) -> | |||||
debignum_arr(Vals, [debignum(V) | Acc]). | |||||
init() -> | |||||
SoName = case code:priv_dir(?MODULE) of | |||||
{error, bad_name} -> | |||||
case filelib:is_dir(filename:join(["..", priv])) of | |||||
true -> | |||||
filename:join(["..", priv, ?MODULE]); | |||||
_ -> | |||||
filename:join([priv, ?MODULE]) | |||||
end; | |||||
Dir -> | |||||
filename:join(Dir, ?MODULE) | |||||
end, | |||||
erlang:load_nif(SoName, 0). | |||||
not_loaded(Line) -> | |||||
exit({not_loaded, [{module, ?MODULE}, {line, Line}]}). |
@ -0,0 +1,33 @@ | |||||
#! /usr/bin/env escript | |||||
main([]) -> | |||||
code:add_pathz("test"), | |||||
code:add_pathz("ebin"), | |||||
Cases = read_cases(), | |||||
etap:plan(length(Cases)), | |||||
lists:foreach(fun(Case) -> test(Case) end, Cases), | |||||
etap:end_tests(). | |||||
test({Name, Json, Erl}) -> | |||||
etap:is(jiffy:decode(Json), Erl, Name). | |||||
read_cases() -> | |||||
CasesPath = filename:join(["test", "cases", "*.json"]), | |||||
FileNames = lists:sort(filelib:wildcard(CasesPath)), | |||||
lists:map(fun(F) -> make_pair(F) end, FileNames). | |||||
make_pair(FileName) -> | |||||
{ok, Json} = file:read_file(FileName), | |||||
{BaseName, _} = lists:splitwith(fun(C) -> C /= $. end, FileName), | |||||
ErlFname = BaseName ++ ".erl", | |||||
{ok, [Term]} = file:consult(ErlFname), | |||||
case Term of | |||||
{error, _} -> | |||||
{BaseName, Json, Term}; | |||||
{error, _, _} -> | |||||
{BaseName, Json, Term}; | |||||
_ -> | |||||
{BaseName, Json, {ok, Term}} | |||||
end. |
@ -0,0 +1,19 @@ | |||||
#! /usr/bin/env escript | |||||
main([]) -> | |||||
code:add_pathz("ebin"), | |||||
code:add_pathz("test"), | |||||
etap:plan(6), | |||||
etap:is(jiffy:decode(<<"true">>), {ok, true}, "DEC: true -> true"), | |||||
etap:is(jiffy:encode(true), {ok, <<"true">>}, "ENC: true -> true"), | |||||
etap:is(jiffy:decode(<<"false">>), {ok, false}, "DEC: false -> false"), | |||||
etap:is(jiffy:encode(false), {ok, <<"false">>}, "ENC: false -> false"), | |||||
etap:is(jiffy:decode(<<"null">>), {ok, null}, "DEC: null -> null"), | |||||
etap:is(jiffy:encode(null), {ok, <<"null">>}, "ENC: null -> null"), | |||||
etap:end_tests(). | |||||
@ -0,0 +1,47 @@ | |||||
#! /usr/bin/env escript | |||||
main([]) -> | |||||
code:add_pathz("ebin"), | |||||
code:add_pathz("test"), | |||||
etap:plan(47), | |||||
util:test_good(good()), | |||||
util:test_errors(errors()), | |||||
etap:end_tests(). | |||||
good() -> | |||||
[ | |||||
{<<"0">>, 0}, | |||||
{<<"-0">>, 0, <<"0">>}, | |||||
{<<"1">>, 1}, | |||||
{<<"12">>, 12}, | |||||
{<<"-3">>, -3}, | |||||
{<<"309230948234098">>, 309230948234098}, | |||||
{<<"1.0">>, 1.0, <<"1">>}, | |||||
{<<"0.3">>, 0.3}, | |||||
{<<"2.4234324">>, 2.4234324, <<"2.42343">>}, | |||||
{<<"-3.1416">>, -3.1416}, | |||||
{<<"1E4">>, 10000.0, <<"10000">>}, | |||||
{<<"1.0E+01">>, 10.0, <<"10">>}, | |||||
{<<"1e1">>, 10.0, <<"10">>}, | |||||
{<<"3.0E2">>, 300.0, <<"300">>}, | |||||
{<<"0E3">>, 0.0, <<"0">>}, | |||||
{<<"1.5E3">>, 1500.0, <<"1500">>}, | |||||
{<<"1.5E-1">>, 0.15, <<"0.15">>}, | |||||
{<<"-0.323E+2">>, -32.3, <<"-32.3">>} | |||||
]. | |||||
errors() -> | |||||
[ | |||||
<<"02">>, | |||||
<<"-01">>, | |||||
<<"+12">>, | |||||
<<"-">>, | |||||
<<"1.">>, | |||||
<<".1">>, | |||||
<<"1.-1">>, | |||||
<<"1E">>, | |||||
<<"1-E2">>, | |||||
<<"2E +3">>, | |||||
<<"1EA">> | |||||
]. |
@ -0,0 +1,36 @@ | |||||
#! /usr/bin/env escript | |||||
main([]) -> | |||||
code:add_pathz("ebin"), | |||||
code:add_pathz("test"), | |||||
etap:plan(21), | |||||
util:test_good(good()), | |||||
util:test_errors(errors()), | |||||
etap:end_tests(). | |||||
good() -> | |||||
[ | |||||
{<<"\"\"">>, <<"">>}, | |||||
{<<"\"0\"">>, <<"0">>}, | |||||
{<<"\"foo\"">>, <<"foo">>}, | |||||
{<<"\"\\\"foobar\\\"\"">>, <<"\"foobar\"">>}, | |||||
{<<"\"\\n\\n\\n\"">>, <<"\n\n\n">>}, | |||||
{<<"\"\\\" \\b\\f\\r\\n\\t\\\"\"">>, <<"\" \b\f\r\n\t\"">>}, | |||||
{<<"\"foo\\u0005bar\"">>, <<"foo", 5, "bar">>}, | |||||
{ | |||||
<<"\"\\uD834\\uDD1E\"">>, | |||||
<<240, 157, 132, 158>>, | |||||
<<34, 240, 157, 132, 158, 34>> | |||||
} | |||||
]. | |||||
errors() -> | |||||
[ | |||||
<<"\"", 0, "\"">>, | |||||
<<"\"\\g\"">>, | |||||
<<"\"\\uFFFF\"">>, | |||||
<<"\"\\uD834foo\\uDD1E\"">>, | |||||
% CouchDB-345 | |||||
<<"\"",78,69,73,77,69,78,32,70,216,82,82,32,70,65,69,78,33,"\"">> | |||||
]. |
@ -0,0 +1,34 @@ | |||||
#! /usr/bin/env escript | |||||
main([]) -> | |||||
code:add_pathz("ebin"), | |||||
code:add_pathz("test"), | |||||
etap:plan(18), | |||||
util:test_good(good()), | |||||
util:test_errors(errors()), | |||||
etap:end_tests(). | |||||
good() -> | |||||
[ | |||||
{<<"[]">>, []}, | |||||
{<<"[\t[\n]\r]">>, [[]], <<"[[]]">>}, | |||||
{<<"[\t123, \r true\n]">>, [123, true], <<"[123,true]">>}, | |||||
{<<"[1,\"foo\"]">>, [1, <<"foo">>]}, | |||||
{<<"[1199344435545.0,1]">>, [1199344435545.0,1], <<"[1.19934e+12,1]">>}, | |||||
{ | |||||
<<"[\"\\u00A1\",\"\\u00FC\"]">>, | |||||
[<<194, 161>>, <<195, 188>>], | |||||
<<"[\"", 194, 161, "\",\"", 195, 188, "\"]">> | |||||
} | |||||
]. | |||||
errors() -> | |||||
[ | |||||
<<"[">>, | |||||
<<"]">>, | |||||
<<"[,]">>, | |||||
<<"[123">>, | |||||
<<"[123,]">>, | |||||
<<"[32 true]">> | |||||
]. |
@ -0,0 +1,34 @@ | |||||
#! /usr/bin/env escript | |||||
main([]) -> | |||||
code:add_pathz("ebin"), | |||||
code:add_pathz("test"), | |||||
etap:plan(15), | |||||
util:test_good(good()), | |||||
util:test_errors(errors()), | |||||
etap:end_tests(). | |||||
good() -> | |||||
[ | |||||
{<<"{}">>, {[]}}, | |||||
{<<"{\"foo\": \"bar\"}">>, | |||||
{[{<<"foo">>, <<"bar">>}]}, | |||||
<<"{\"foo\":\"bar\"}">>}, | |||||
{<<"\n\n{\"foo\":\r \"bar\",\n \"baz\"\t: 123 }">>, | |||||
{[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]}, | |||||
<<"{\"foo\":\"bar\",\"baz\":123}">>} | |||||
]. | |||||
errors() -> | |||||
[ | |||||
<<"{">>, | |||||
<<"{,}">>, | |||||
<<"{123:true}">>, | |||||
<<"{false:123}">>, | |||||
<<"{:\"stuff\"}">>, | |||||
<<"{\"key\":}">>, | |||||
<<"{\"key\": 123">>, | |||||
<<"{\"key\": 123 true">>, | |||||
<<"{\"key\": 123,}">> | |||||
]. |
@ -0,0 +1,39 @@ | |||||
#! /usr/bin/env escript | |||||
main([]) -> | |||||
code:add_pathz("ebin"), | |||||
code:add_pathz("test"), | |||||
etap:plan(12), | |||||
util:test_good(good()), | |||||
util:test_errors(errors()), | |||||
etap:end_tests(). | |||||
good() -> | |||||
[ | |||||
{<<"[{}]">>, [{[]}]}, | |||||
{<<"{\"foo\":[123]}">>, {[{<<"foo">>, [123]}]}}, | |||||
{<<"{\"foo\":{\"bar\":true}}">>, | |||||
{[{<<"foo">>, {[{<<"bar">>, true}]} }]} }, | |||||
{<<"{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}">>, | |||||
{[ | |||||
{<<"foo">>, []}, | |||||
{<<"bar">>, {[{<<"baz">>, true}]}}, | |||||
{<<"alice">>, <<"bob">>} | |||||
]} | |||||
}, | |||||
{<<"[-123,\"foo\",{\"bar\":[]},null]">>, | |||||
[ | |||||
-123, | |||||
<<"foo">>, | |||||
{[{<<"bar">>, []}]}, | |||||
null | |||||
] | |||||
} | |||||
]. | |||||
errors() -> | |||||
[ | |||||
<<"[{}">>, | |||||
<<"}]">> | |||||
]. |
@ -0,0 +1,16 @@ | |||||
[ | |||||
<<"foo">>, | |||||
<<"bar">>, | |||||
<<"baz">>, | |||||
true, | |||||
false, | |||||
null, | |||||
{[{<<"key">>, <<"value">>}]}, | |||||
[ | |||||
null, | |||||
null, | |||||
null, | |||||
[] | |||||
], | |||||
<<"\n\r\\">> | |||||
]. |
@ -0,0 +1,6 @@ | |||||
["foo", | |||||
"bar", "baz", | |||||
true,false,null,{"key":"value"}, | |||||
[null,null,null,[]], | |||||
"\n\r\\" | |||||
] |
@ -0,0 +1 @@ | |||||
{error,{1,invalid_json}}. |
@ -0,0 +1 @@ | |||||
] |
@ -0,0 +1 @@ | |||||
{error,{3,truncated_json}}. |
@ -0,0 +1 @@ | |||||
[ |
@ -0,0 +1 @@ | |||||
{error,{97,invalid_literal}}. |
@ -0,0 +1,4 @@ | |||||
["this","is","what","should","be", | |||||
"a happy bit of json", | |||||
"but someone, misspelled \"true\"", ture, | |||||
"who says JSON is easy for humans to generate?"] |
@ -0,0 +1 @@ | |||||
<<77, 208, 176, 228, 186, 140, 240, 144, 140, 130>>. |
@ -0,0 +1 @@ | |||||
"\u004d\u0430\u4e8c\ud800\udf02" |
@ -0,0 +1 @@ | |||||
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]. |
@ -0,0 +1 @@ | |||||
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] |
@ -0,0 +1,19 @@ | |||||
{[ | |||||
{<<"glossary">>, {[ | |||||
{<<"title">>, <<"example glossary">>}, | |||||
{<<"GlossDiv">>, {[ | |||||
{<<"title">>, <<"S">>}, | |||||
{<<"GlossList">>, [ | |||||
{[ | |||||
{<<"ID">>, <<"SGML">>}, | |||||
{<<"SortAs">>, <<"SGML">>}, | |||||
{<<"GlossTerm">>, <<"Standard Generalized Markup Language">>}, | |||||
{<<"Acronym">>, <<"SGML">>}, | |||||
{<<"Abbrev">>, <<"ISO 8879:1986">>}, | |||||
{<<"GlossDef">>, <<"A meta-markup language, used to create markup languages such as DocBook.">>}, | |||||
{<<"GlossSeeAlso">>, [<<"GML">>, <<"XML">>, <<"markup">>]} | |||||
]} | |||||
]} | |||||
]}} | |||||
]}} | |||||
]}. |
@ -0,0 +1 @@ | |||||
{ "glossary": { "title": "example glossary", "GlossDiv": { "title": "S", "GlossList": [ { "ID": "SGML", "SortAs": "SGML", "GlossTerm": "Standard Generalized Markup Language", "Acronym": "SGML", "Abbrev": "ISO 8879:1986", "GlossDef": "A meta-markup language, used to create markup languages such as DocBook.", "GlossSeeAlso": ["GML", "XML", "markup"] } ] } } } |
@ -0,0 +1 @@ | |||||
[10, 10, 3.141569, 1000]. |
@ -0,0 +1 @@ | |||||
[ 0.1e2, 1e1, 3.141569, 10000000000000e-10] |
@ -0,0 +1 @@ | |||||
[]. |
@ -0,0 +1 @@ | |||||
[] |
@ -0,0 +1 @@ | |||||
<<"">>. |
@ -0,0 +1 @@ | |||||
"" |
@ -0,0 +1,6 @@ | |||||
[ | |||||
<<208, 148, 208, 176>>, | |||||
<<208, 156, 209, 131>>, | |||||
<<208, 149, 208, 177, 208, 176>>, | |||||
<<208, 156, 208, 176, 208, 185, 208, 186, 208, 176, 209, 130, 208, 176>> | |||||
]. |
@ -0,0 +1,4 @@ | |||||
["\u0414\u0430", | |||||
"\u041c\u0443", | |||||
"\u0415\u0431\u0430", | |||||
"\u041c\u0430\u0439\u043a\u0430\u0442\u0430"] |
@ -0,0 +1 @@ | |||||
<<"foobar">>. |
@ -0,0 +1 @@ | |||||
"\u0066\u006f\u006f\u0062\u0061\u0072" |
@ -0,0 +1 @@ | |||||
false. |
@ -0,0 +1 @@ | |||||
false |
@ -0,0 +1 @@ | |||||
{error,{6,invalid_trailing_data}}. |
@ -0,0 +1 @@ | |||||
falsex |
@ -0,0 +1 @@ | |||||
{[{<<"U+10ABCD">>, <<244, 138, 175, 141>>}]}. |
@ -0,0 +1,2 @@ | |||||
{ "U+10ABCD": "" } | |||||
@ -0,0 +1,13 @@ | |||||
[ | |||||
1, | |||||
2, | |||||
3, | |||||
4, | |||||
5, | |||||
6, | |||||
7, | |||||
123456789, | |||||
-123456789, | |||||
2147483647, | |||||
-2147483647 | |||||
]. |
@ -0,0 +1,3 @@ | |||||
[ 1,2,3,4,5,6,7, | |||||
123456789 , -123456789, | |||||
2147483647, -2147483647 ] |
@ -0,0 +1 @@ | |||||
{error,{10,invalid_string}}. |
@ -0,0 +1 @@ | |||||
["Да М� Еба Майката"] |
@ -0,0 +1 @@ | |||||
{error,{8,invalid_string}}. |
@ -0,0 +1 @@ | |||||
"\ud800" |
@ -0,0 +1 @@ | |||||
{error,{17,invalid_json}}. |
@ -0,0 +1 @@ | |||||
{ "bad thing": 01 } |
@ -0,0 +1 @@ | |||||
{error,{83,invalid_number}}. |
@ -0,0 +1,7 @@ | |||||
[ | |||||
"foo", true, | |||||
true, "blue", | |||||
"baby where are you?", "oh boo hoo!", | |||||
- | |||||
] | |||||
@ -0,0 +1 @@ | |||||
123456789. |
@ -0,0 +1 @@ | |||||
123456789 |
@ -0,0 +1 @@ | |||||
{error,{1,invalid_json}}. |
@ -0,0 +1 @@ | |||||
} |
@ -0,0 +1 @@ | |||||
{error,{3,truncated_json}}. |
@ -0,0 +1 @@ | |||||
{ |
@ -0,0 +1 @@ | |||||
{error,{4,invalid_number}}. |
@ -0,0 +1 @@ | |||||
10.e2 |
@ -0,0 +1 @@ | |||||
{error,{4,invalid_number}}. |
@ -0,0 +1 @@ | |||||
10e |
@ -0,0 +1 @@ | |||||
{error,{125,invalid_string}}. |
@ -0,0 +1 @@ | |||||
{"CoreletAPIVersion":2,"CoreletType":"standalone","documentation":"A corelet that provides the capability to upload a folder’s contents into a user’s locker.","functions":[{"documentation":"Displays a dialog box that allows user to select a folder on the local system.","name":"ShowBrowseDialog","parameters":[{"documentation":"The callback function for results.","name":"callback","required":true,"type":"callback"}]},{"documentation":"Uploads all mp3 files in the folder provided.","name":"UploadFolder","parameters":[{"documentation":"The path to upload mp3 files from.","name":"path","required":true,"type":"string"},{"documentation":"The callback function for progress.","name":"callback","required":true,"type":"callback"}]},{"documentation":"Returns the server name to the current locker service.","name":"GetLockerService","parameters":[]},{"documentation":"Changes the name of the locker service.","name":"SetLockerService","parameters":[{"documentation":"The value of the locker service to set active.","name":"LockerService","required":true,"type":"string"}]},{"documentation":"Downloads locker files to the suggested folder.","name":"DownloadFile","parameters":[{"documentation":"The origin path of the locker file.","name":"path","required":true,"type":"string"},{"documentation":"The Window destination path of the locker file.","name":"destination","required":true,"type":"integer"},{"documentation":"The callback function for progress.","name":"callback","required":true,"type":"callback"}]}],"name":"LockerUploader","version":{"major":0,"micro":1,"minor":0},"versionString":"0.0.1"} |
@ -0,0 +1 @@ | |||||
null. |
@ -0,0 +1 @@ | |||||
null |
@ -0,0 +1 @@ | |||||
{error,{5,invalid_trailing_data}}. |
@ -0,0 +1 @@ | |||||
nullx |
@ -0,0 +1,5 @@ | |||||
{[ | |||||
{<<"boolean, true">>, true}, | |||||
{<<"boolean, false">>, false}, | |||||
{<<"null">>, null} | |||||
]}. |
@ -0,0 +1,5 @@ | |||||
{ | |||||
"boolean, true": true, | |||||
"boolean, false": false, | |||||
"null": null | |||||
} |
@ -0,0 +1,5 @@ | |||||
{[ | |||||
{<<"this">>, <<"is">>}, | |||||
{<<"really">>, <<"simple">>}, | |||||
{<<"json">>, <<"right?">>} | |||||
]}. |
@ -0,0 +1,5 @@ | |||||
{ | |||||
"this": "is", | |||||
"really": "simple", | |||||
"json": "right?" | |||||
} |
@ -0,0 +1 @@ | |||||
{error,{63,invalid_string}}. |
@ -0,0 +1 @@ | |||||
["\n foo \/ bar \r\f\\\ufffd\t\b\"\\ and you can't escape thi\s"] |
@ -0,0 +1 @@ | |||||
{error,{48,invalid_string}}. |
@ -0,0 +1 @@ | |||||
"foo foo, blah blah \u0123 \u4567 \u89ab \uc/ef \uABCD \uEFFE bar baz bing" |
@ -0,0 +1,5 @@ | |||||
[ | |||||
<<"\n foo \/ bar \r\f\\", 239, 191, 186, "\t\b\"\\">>, | |||||
<<"\"and this string has an escape at the beginning">>, | |||||
<<"and this string has no escapes">> | |||||
]. |
@ -0,0 +1,3 @@ | |||||
["\n foo \/ bar \r\f\\\ufffa\t\b\"\\", | |||||
"\"and this string has an escape at the beginning", | |||||
"and this string has no escapes" ] |
@ -0,0 +1 @@ | |||||
{error,{67,invalid_string}}. |
@ -0,0 +1,2 @@ | |||||
"la di dah. this is a string, and I can do this, \n, but not this | |||||
" |
@ -0,0 +1,4 @@ | |||||
{[ | |||||
{<<"matzue">>, <<230, 157, 190, 230, 177, 159>>}, | |||||
{<<"asakusa">>, <<230, 181, 133, 232, 141, 137>>} | |||||
]}. |
@ -0,0 +1 @@ | |||||
{"matzue": "松江", "asakusa": "浅草"} |
@ -0,0 +1 @@ | |||||
true. |
@ -0,0 +1 @@ | |||||
true |
@ -0,0 +1 @@ | |||||
{error,{5,invalid_trailing_data}}. |
@ -0,0 +1 @@ | |||||
truex |
@ -0,0 +1,5 @@ | |||||
[ | |||||
<<208, 148, 208, 176, 32, 208, 156, 209, 131, 32, 208, 149, 208, | |||||
177, 208, 176, 32, 208, 156, 208, 176, 208, 185, 208, 186, 208, | |||||
176, 209, 130, 208, 176>> | |||||
]. |
@ -0,0 +1 @@ | |||||
["Да Му Еба Майката"] |
@ -0,0 +1,612 @@ | |||||
%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> | |||||
%% | |||||
%% Permission is hereby granted, free of charge, to any person | |||||
%% obtaining a copy of this software and associated documentation | |||||
%% files (the "Software"), to deal in the Software without | |||||
%% restriction, including without limitation the rights to use, | |||||
%% copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
%% copies of the Software, and to permit persons to whom the | |||||
%% Software is furnished to do so, subject to the following | |||||
%% conditions: | |||||
%% | |||||
%% The above copyright notice and this permission notice shall be | |||||
%% included in all copies or substantial portions of the Software. | |||||
%% | |||||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |||||
%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |||||
%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |||||
%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |||||
%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |||||
%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |||||
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |||||
%% OTHER DEALINGS IN THE SOFTWARE. | |||||
%% | |||||
%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] | |||||
%% @author Jeremy Wall <jeremy@marzhillstudios.com> | |||||
%% @version 0.3.4 | |||||
%% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines | |||||
%% @reference http://testanything.org/wiki/index.php/Main_Page | |||||
%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol | |||||
%% @todo Finish implementing the skip directive. | |||||
%% @todo Document the messages handled by this receive loop. | |||||
%% @todo Explain in documentation why we use a process to handle test input. | |||||
%% @doc etap is a TAP testing module for Erlang components and applications. | |||||
%% This module allows developers to test their software using the TAP method. | |||||
%% | |||||
%% <blockquote cite="http://en.wikipedia.org/wiki/Test_Anything_Protocol"><p> | |||||
%% TAP, the Test Anything Protocol, is a simple text-based interface between | |||||
%% testing modules in a test harness. TAP started life as part of the test | |||||
%% harness for Perl but now has implementations in C/C++, Python, PHP, Perl | |||||
%% and probably others by the time you read this. | |||||
%% </p></blockquote> | |||||
%% | |||||
%% The testing process begins by defining a plan using etap:plan/1, running | |||||
%% a number of etap tests and then calling eta:end_tests/0. Please refer to | |||||
%% the Erlang modules in the t directory of this project for example tests. | |||||
-module(etap). | |||||
-vsn("0.3.4"). | |||||
-export([ | |||||
ensure_test_server/0, | |||||
start_etap_server/0, | |||||
test_server/1, | |||||
msg/1, msg/2, | |||||
diag/1, diag/2, | |||||
expectation_mismatch_message/3, | |||||
plan/1, | |||||
end_tests/0, | |||||
not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3, | |||||
fun_is/3, expect_fun/3, expect_fun/4, | |||||
is_greater/3, | |||||
skip/1, skip/2, | |||||
datetime/1, | |||||
skip/3, | |||||
bail/0, bail/1, | |||||
test_state/0, failure_count/0 | |||||
]). | |||||
-export([ | |||||
contains_ok/3, | |||||
is_before/4 | |||||
]). | |||||
-export([ | |||||
is_pid/2, | |||||
is_alive/2, | |||||
is_mfa/3 | |||||
]). | |||||
-export([ | |||||
loaded_ok/2, | |||||
can_ok/2, can_ok/3, | |||||
has_attrib/2, is_attrib/3, | |||||
is_behaviour/2 | |||||
]). | |||||
-export([ | |||||
dies_ok/2, | |||||
lives_ok/2, | |||||
throws_ok/3 | |||||
]). | |||||
-record(test_state, { | |||||
planned = 0, | |||||
count = 0, | |||||
pass = 0, | |||||
fail = 0, | |||||
skip = 0, | |||||
skip_reason = "" | |||||
}). | |||||
%% @spec plan(N) -> Result | |||||
%% N = unknown | skip | {skip, string()} | integer() | |||||
%% Result = ok | |||||
%% @doc Create a test plan and boot strap the test server. | |||||
plan(unknown) -> | |||||
ensure_test_server(), | |||||
etap_server ! {self(), plan, unknown}, | |||||
ok; | |||||
plan(skip) -> | |||||
io:format("1..0 # skip~n"); | |||||
plan({skip, Reason}) -> | |||||
io:format("1..0 # skip ~s~n", [Reason]); | |||||
plan(N) when is_integer(N), N > 0 -> | |||||
ensure_test_server(), | |||||
etap_server ! {self(), plan, N}, | |||||
ok. | |||||
%% @spec end_tests() -> ok | |||||
%% @doc End the current test plan and output test results. | |||||
%% @todo This should probably be done in the test_server process. | |||||
end_tests() -> | |||||
case whereis(etap_server) of | |||||
undefined -> self() ! true; | |||||
_ -> etap_server ! {self(), state} | |||||
end, | |||||
State = receive X -> X end, | |||||
if | |||||
State#test_state.planned == -1 -> | |||||
io:format("1..~p~n", [State#test_state.count]); | |||||
true -> | |||||
ok | |||||
end, | |||||
case whereis(etap_server) of | |||||
undefined -> ok; | |||||
_ -> etap_server ! done, ok | |||||
end. | |||||
bail() -> | |||||
bail(""). | |||||
bail(Reason) -> | |||||
etap_server ! {self(), diag, "Bail out! " ++ Reason}, | |||||
etap_server ! done, ok, | |||||
ok. | |||||
%% @spec test_state() -> Return | |||||
%% Return = test_state_record() | {error, string()} | |||||
%% @doc Return the current test state | |||||
test_state() -> | |||||
etap_server ! {self(), state}, | |||||
receive | |||||
X when is_record(X, test_state) -> X | |||||
after | |||||
1000 -> {error, "Timed out waiting for etap server reply.~n"} | |||||
end. | |||||
%% @spec failure_count() -> Return | |||||
%% Return = integer() | {error, string()} | |||||
%% @doc Return the current failure count | |||||
failure_count() -> | |||||
case test_state() of | |||||
#test_state{fail=FailureCount} -> FailureCount; | |||||
X -> X | |||||
end. | |||||
%% @spec msg(S) -> ok | |||||
%% S = string() | |||||
%% @doc Print a message in the test output. | |||||
msg(S) -> etap_server ! {self(), diag, S}, ok. | |||||
%% @spec msg(Format, Data) -> ok | |||||
%% Format = atom() | string() | binary() | |||||
%% Data = [term()] | |||||
%% UnicodeList = [Unicode] | |||||
%% Unicode = int() | |||||
%% @doc Print a message in the test output. | |||||
%% Function arguments are passed through io_lib:format/2. | |||||
msg(Format, Data) -> msg(io_lib:format(Format, Data)). | |||||
%% @spec diag(S) -> ok | |||||
%% S = string() | |||||
%% @doc Print a debug/status message related to the test suite. | |||||
diag(S) -> msg("# " ++ S). | |||||
%% @spec diag(Format, Data) -> ok | |||||
%% Format = atom() | string() | binary() | |||||
%% Data = [term()] | |||||
%% UnicodeList = [Unicode] | |||||
%% Unicode = int() | |||||
%% @doc Print a debug/status message related to the test suite. | |||||
%% Function arguments are passed through io_lib:format/2. | |||||
diag(Format, Data) -> diag(io_lib:format(Format, Data)). | |||||
%% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok | |||||
%% Got = any() | |||||
%% Expected = any() | |||||
%% Desc = string() | |||||
%% @doc Print an expectation mismatch message in the test output. | |||||
expectation_mismatch_message(Got, Expected, Desc) -> | |||||
msg(" ---"), | |||||
msg(" description: ~p", [Desc]), | |||||
msg(" found: ~p", [Got]), | |||||
msg(" wanted: ~p", [Expected]), | |||||
msg(" ..."), | |||||
ok. | |||||
% @spec evaluate(Pass, Got, Expected, Desc) -> Result | |||||
%% Pass = true | false | |||||
%% Got = any() | |||||
%% Expected = any() | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Evaluate a test statement, printing an expectation mismatch message | |||||
%% if the test failed. | |||||
evaluate(Pass, Got, Expected, Desc) -> | |||||
case mk_tap(Pass, Desc) of | |||||
false -> | |||||
expectation_mismatch_message(Got, Expected, Desc), | |||||
false; | |||||
true -> | |||||
true | |||||
end. | |||||
%% @spec ok(Expr, Desc) -> Result | |||||
%% Expr = true | false | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that a statement is true. | |||||
ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc). | |||||
%% @spec not_ok(Expr, Desc) -> Result | |||||
%% Expr = true | false | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that a statement is false. | |||||
not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc). | |||||
%% @spec is_ok(Expr, Desc) -> Result | |||||
%% Expr = any() | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that two values are the same. | |||||
is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc). | |||||
%% @spec is(Got, Expected, Desc) -> Result | |||||
%% Got = any() | |||||
%% Expected = any() | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that two values are the same. | |||||
is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc). | |||||
%% @spec isnt(Got, Expected, Desc) -> Result | |||||
%% Got = any() | |||||
%% Expected = any() | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that two values are not the same. | |||||
isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc). | |||||
%% @spec is_greater(ValueA, ValueB, Desc) -> Result | |||||
%% ValueA = number() | |||||
%% ValueB = number() | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that an integer is greater than another. | |||||
is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> | |||||
mk_tap(ValueA > ValueB, Desc). | |||||
%% @spec any(Got, Items, Desc) -> Result | |||||
%% Got = any() | |||||
%% Items = [any()] | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that an item is in a list. | |||||
any(Got, Items, Desc) when is_function(Got) -> | |||||
is(lists:any(Got, Items), true, Desc); | |||||
any(Got, Items, Desc) -> | |||||
is(lists:member(Got, Items), true, Desc). | |||||
%% @spec none(Got, Items, Desc) -> Result | |||||
%% Got = any() | |||||
%% Items = [any()] | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Assert that an item is not in a list. | |||||
none(Got, Items, Desc) when is_function(Got) -> | |||||
is(lists:any(Got, Items), false, Desc); | |||||
none(Got, Items, Desc) -> | |||||
is(lists:member(Got, Items), false, Desc). | |||||
%% @spec fun_is(Fun, Expected, Desc) -> Result | |||||
%% Fun = function() | |||||
%% Expected = any() | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Use an anonymous function to assert a pattern match. | |||||
fun_is(Fun, Expected, Desc) when is_function(Fun) -> | |||||
is(Fun(Expected), true, Desc). | |||||
%% @spec expect_fun(ExpectFun, Got, Desc) -> Result | |||||
%% ExpectFun = function() | |||||
%% Got = any() | |||||
%% Desc = string() | |||||
%% Result = true | false | |||||
%% @doc Use an anonymous function to assert a pattern match, using actual | |||||
%% value as the argument to the function. | |||||
expect_fun(ExpectFun, Got, Desc) -> | |||||
evaluate(ExpectFun(Got), Got, ExpectFun, Desc). | |||||
%% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result | |||||
%% ExpectFun = function() | |||||
%% Got = any() | |||||
%% Desc = string() | |||||
%% ExpectStr = string() | |||||
%% Result = true | false | |||||
%% @doc Use an anonymous function to assert a pattern match, using actual | |||||
%% value as the argument to the function. | |||||
expect_fun(ExpectFun, Got, Desc, ExpectStr) -> | |||||
evaluate(ExpectFun(Got), Got, ExpectStr, Desc). | |||||
%% @equiv skip(TestFun, "") | |||||
skip(TestFun) when is_function(TestFun) -> | |||||
skip(TestFun, ""). | |||||
%% @spec skip(TestFun, Reason) -> ok | |||||
%% TestFun = function() | |||||
%% Reason = string() | |||||
%% @doc Skip a test. | |||||
skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) -> | |||||
begin_skip(Reason), | |||||
catch TestFun(), | |||||
end_skip(), | |||||
ok. | |||||
%% @spec skip(Q, TestFun, Reason) -> ok | |||||
%% Q = true | false | function() | |||||
%% TestFun = function() | |||||
%% Reason = string() | |||||
%% @doc Skips a test conditionally. The first argument to this function can | |||||
%% either be the 'true' or 'false' atoms or a function that returns 'true' or | |||||
%% 'false'. | |||||
skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) -> | |||||
case QFun() of | |||||
true -> begin_skip(Reason), TestFun(), end_skip(); | |||||
_ -> TestFun() | |||||
end, | |||||
ok; | |||||
skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true -> | |||||
begin_skip(Reason), | |||||
TestFun(), | |||||
end_skip(), | |||||
ok; | |||||
skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) -> | |||||
TestFun(), | |||||
ok. | |||||
%% @private | |||||
begin_skip(Reason) -> | |||||
etap_server ! {self(), begin_skip, Reason}. | |||||
%% @private | |||||
end_skip() -> | |||||
etap_server ! {self(), end_skip}. | |||||
%% @spec contains_ok(string(), string(), string()) -> true | false | |||||
%% @doc Assert that a string is contained in another string. | |||||
contains_ok(Source, String, Desc) -> | |||||
etap:isnt( | |||||
string:str(Source, String), | |||||
0, | |||||
Desc | |||||
). | |||||
%% @spec is_before(string(), string(), string(), string()) -> true | false | |||||
%% @doc Assert that a string comes before another string within a larger body. | |||||
is_before(Source, StringA, StringB, Desc) -> | |||||
etap:is_greater( | |||||
string:str(Source, StringB), | |||||
string:str(Source, StringA), | |||||
Desc | |||||
). | |||||
%% @doc Assert that a given variable is a pid. | |||||
is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); | |||||
is_pid(_, Desc) -> etap:ok(false, Desc). | |||||
%% @doc Assert that a given process/pid is alive. | |||||
is_alive(Pid, Desc) -> | |||||
etap:ok(erlang:is_process_alive(Pid), Desc). | |||||
%% @doc Assert that the current function of a pid is a given {M, F, A} tuple. | |||||
is_mfa(Pid, MFA, Desc) -> | |||||
etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). | |||||
%% @spec loaded_ok(atom(), string()) -> true | false | |||||
%% @doc Assert that a module has been loaded successfully. | |||||
loaded_ok(M, Desc) when is_atom(M) -> | |||||
etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). | |||||
%% @spec can_ok(atom(), atom()) -> true | false | |||||
%% @doc Assert that a module exports a given function. | |||||
can_ok(M, F) when is_atom(M), is_atom(F) -> | |||||
Matches = [X || {X, _} <- M:module_info(exports), X == F], | |||||
etap:ok(Matches > 0, lists:concat([M, " can ", F])). | |||||
%% @spec can_ok(atom(), atom(), integer()) -> true | false | |||||
%% @doc Assert that a module exports a given function with a given arity. | |||||
can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> | |||||
Matches = [X || X <- M:module_info(exports), X == {F, A}], | |||||
etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). | |||||
%% @spec has_attrib(M, A) -> true | false | |||||
%% M = atom() | |||||
%% A = atom() | |||||
%% @doc Asserts that a module has a given attribute. | |||||
has_attrib(M, A) when is_atom(M), is_atom(A) -> | |||||
etap:isnt( | |||||
proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), | |||||
'asdlkjasdlkads', | |||||
lists:concat([M, " has attribute ", A]) | |||||
). | |||||
%% @spec has_attrib(M, A. V) -> true | false | |||||
%% M = atom() | |||||
%% A = atom() | |||||
%% V = any() | |||||
%% @doc Asserts that a module has a given attribute with a given value. | |||||
is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> | |||||
etap:is( | |||||
proplists:get_value(A, M:module_info(attributes)), | |||||
[V], | |||||
lists:concat([M, "'s ", A, " is ", V]) | |||||
). | |||||
%% @spec is_behavior(M, B) -> true | false | |||||
%% M = atom() | |||||
%% B = atom() | |||||
%% @doc Asserts that a given module has a specific behavior. | |||||
is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> | |||||
is_attrib(M, behaviour, B). | |||||
%% @doc Assert that an exception is raised when running a given function. | |||||
dies_ok(F, Desc) -> | |||||
case (catch F()) of | |||||
{'EXIT', _} -> etap:ok(true, Desc); | |||||
_ -> etap:ok(false, Desc) | |||||
end. | |||||
%% @doc Assert that an exception is not raised when running a given function. | |||||
lives_ok(F, Desc) -> | |||||
etap:is(try_this(F), success, Desc). | |||||
%% @doc Assert that the exception thrown by a function matches the given exception. | |||||
throws_ok(F, Exception, Desc) -> | |||||
try F() of | |||||
_ -> etap:ok(nok, Desc) | |||||
catch | |||||
_:E -> | |||||
etap:is(E, Exception, Desc) | |||||
end. | |||||
%% @private | |||||
%% @doc Run a function and catch any exceptions. | |||||
try_this(F) when is_function(F, 0) -> | |||||
try F() of | |||||
_ -> success | |||||
catch | |||||
throw:E -> {throw, E}; | |||||
error:E -> {error, E}; | |||||
exit:E -> {exit, E} | |||||
end. | |||||
%% @private | |||||
%% @doc Start the etap_server process if it is not running already. | |||||
ensure_test_server() -> | |||||
case whereis(etap_server) of | |||||
undefined -> | |||||
proc_lib:start(?MODULE, start_etap_server,[]); | |||||
_ -> | |||||
diag("The test server is already running.") | |||||
end. | |||||
%% @private | |||||
%% @doc Start the etap_server loop and register itself as the etap_server | |||||
%% process. | |||||
start_etap_server() -> | |||||
catch register(etap_server, self()), | |||||
proc_lib:init_ack(ok), | |||||
etap:test_server(#test_state{ | |||||
planned = 0, | |||||
count = 0, | |||||
pass = 0, | |||||
fail = 0, | |||||
skip = 0, | |||||
skip_reason = "" | |||||
}). | |||||
%% @private | |||||
%% @doc The main etap_server receive/run loop. The etap_server receive loop | |||||
%% responds to seven messages apperatining to failure or passing of tests. | |||||
%% It is also used to initiate the testing process with the {_, plan, _} | |||||
%% message that clears the current test state. | |||||
test_server(State) -> | |||||
NewState = receive | |||||
{_From, plan, unknown} -> | |||||
io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), | |||||
io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), | |||||
State#test_state{ | |||||
planned = -1, | |||||
count = 0, | |||||
pass = 0, | |||||
fail = 0, | |||||
skip = 0, | |||||
skip_reason = "" | |||||
}; | |||||
{_From, plan, N} -> | |||||
io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), | |||||
io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), | |||||
io:format("1..~p~n", [N]), | |||||
State#test_state{ | |||||
planned = N, | |||||
count = 0, | |||||
pass = 0, | |||||
fail = 0, | |||||
skip = 0, | |||||
skip_reason = "" | |||||
}; | |||||
{_From, begin_skip, Reason} -> | |||||
State#test_state{ | |||||
skip = 1, | |||||
skip_reason = Reason | |||||
}; | |||||
{_From, end_skip} -> | |||||
State#test_state{ | |||||
skip = 0, | |||||
skip_reason = "" | |||||
}; | |||||
{_From, pass, Desc} -> | |||||
FullMessage = skip_diag( | |||||
" - " ++ Desc, | |||||
State#test_state.skip, | |||||
State#test_state.skip_reason | |||||
), | |||||
io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), | |||||
State#test_state{ | |||||
count = State#test_state.count + 1, | |||||
pass = State#test_state.pass + 1 | |||||
}; | |||||
{_From, fail, Desc} -> | |||||
FullMessage = skip_diag( | |||||
" - " ++ Desc, | |||||
State#test_state.skip, | |||||
State#test_state.skip_reason | |||||
), | |||||
io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), | |||||
State#test_state{ | |||||
count = State#test_state.count + 1, | |||||
fail = State#test_state.fail + 1 | |||||
}; | |||||
{From, state} -> | |||||
From ! State, | |||||
State; | |||||
{_From, diag, Message} -> | |||||
io:format("~s~n", [Message]), | |||||
State; | |||||
{From, count} -> | |||||
From ! State#test_state.count, | |||||
State; | |||||
{From, is_skip} -> | |||||
From ! State#test_state.skip, | |||||
State; | |||||
done -> | |||||
exit(normal) | |||||
end, | |||||
test_server(NewState). | |||||
%% @private | |||||
%% @doc Process the result of a test and send it to the etap_server process. | |||||
mk_tap(Result, Desc) -> | |||||
IsSkip = lib:sendw(etap_server, is_skip), | |||||
case [IsSkip, Result] of | |||||
[_, true] -> | |||||
etap_server ! {self(), pass, Desc}, | |||||
true; | |||||
[1, _] -> | |||||
etap_server ! {self(), pass, Desc}, | |||||
true; | |||||
_ -> | |||||
etap_server ! {self(), fail, Desc}, | |||||
false | |||||
end. | |||||
%% @private | |||||
%% @doc Format a date/time string. | |||||
datetime(DateTime) -> | |||||
{{Year, Month, Day}, {Hour, Min, Sec}} = DateTime, | |||||
io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B", [Year, Month, Day, Hour, Min, Sec]). | |||||
%% @private | |||||
%% @doc Craft an output message taking skip/todo into consideration. | |||||
skip_diag(Message, 0, _) -> | |||||
Message; | |||||
skip_diag(_Message, 1, "") -> | |||||
" # SKIP"; | |||||
skip_diag(_Message, 1, Reason) -> | |||||
" # SKIP : " ++ Reason. |
@ -0,0 +1,31 @@ | |||||
-module(util). | |||||
-export([test_good/1, test_errors/1]). | |||||
test_good(Cases) -> | |||||
lists:foreach(fun(Case) -> check_good(Case) end, Cases). | |||||
test_errors(Cases) -> | |||||
lists:foreach(fun(Case) -> check_error(Case) end, Cases). | |||||
ok_dec(J, _E) -> | |||||
lists:flatten(io_lib:format("Decoded ~p.", [J])). | |||||
ok_enc(E, _J) -> | |||||
lists:flatten(io_lib:format("Encoded ~p", [E])). | |||||
error_mesg(J) -> | |||||
lists:flatten(io_lib:format("Decoding ~p returns an error.", [J])). | |||||
check_good({J, E}) -> | |||||
etap:is(jiffy:decode(J), {ok, E}, ok_dec(J, E)), | |||||
etap:is(jiffy:encode(E), {ok, J}, ok_enc(E, J)); | |||||
check_good({J, E, J2}) -> | |||||
etap:is(jiffy:decode(J), {ok, E}, ok_dec(J, E)), | |||||
etap:is(jiffy:encode(E), {ok, J2}, ok_enc(E, J2)). | |||||
check_error(J) -> | |||||
etap:fun_is( | |||||
fun({error, _}) -> true; (_) -> false end, | |||||
jiffy:decode(J), | |||||
error_mesg(J) | |||||
). |