@ -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) | |||
). |