blob: d8790d089cb9a448aebd29857c09a9d33294244c [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "quickjs.h"
#include "quickjs-libc.h"
#define DEFAULT_STACK_SIZE (64L * 1024L * 1024L)
#define BUF_SIZE 1024
#define USAGE "couchjs [-V|-M memorylimit|-h] <script.js>\n"
#define BAIL(error) {fprintf(stderr, "%s:%d %s\n", __FILE__, __LINE__, error);\
exit(EXIT_FAILURE);\
}
#define BAILJS(cx, error) {fprintf(stderr, "%s:%d %s\n", __FILE__, __LINE__, error);\
js_std_dump_error(cx);\
exit(EXIT_FAILURE);\
}
// These are auto-generated by qjsc
extern const uint32_t bytecode_size;
extern const uint8_t bytecode[];
typedef struct {
int stack_size;
} couch_args;
typedef enum {CMD_EMPTY, CMD_DDOC, CMD_RESET, CMD_LIST, CMD_VIEW} CmdType;
static void parse_args(int argc, const char* argv[], couch_args* args)
{
int i = 1;
while(i < argc) {
if (strncmp("-h", argv[i], 2) == 0) {
fprintf(stderr, USAGE);
exit(0);
} else if (strncmp("-M", argv[i], 2) == 0) {
args->stack_size = atoi(argv[++i]);
if (args->stack_size <= 1L * 1024L * 1024L) {
BAIL("Invalid stack size");
}
} else if (strncmp("-V", argv[i], 2) == 0) {
fprintf(stderr, "quickjs\n");
exit(0);
} else {
break;
}
i++;
}
}
// Parse the command type. We only care about resets, ddoc operations and
// making sure wires were not crossed and we ended up in a list streaming
// sub-command state somehow. See protocol description at:
// https://docs.couchdb.org/en/stable/query-server/protocol.html
//
static CmdType parse_command(char* str, size_t len) {
if (len == 0) {
return CMD_EMPTY;
}
if (len >= 8 && strncmp("[\"reset\"", str, 8) == 0) {
return CMD_RESET;
}
if (len >= 7 && strncmp("[\"ddoc\"", str, 7) == 0) {
return CMD_DDOC;
}
if (len >= 11 && strncmp("[\"list_row\"", str, 11) == 0) {
return CMD_LIST;
}
if (len >= 11 && strncmp("[\"list_end\"", str, 11) == 0) {
return CMD_LIST;
}
return CMD_VIEW;
}
static void add_cx_methods(JSContext* cx) {
//TODO: configure some features with env vars of command line switches
JS_AddIntrinsicBaseObjects(cx);
JS_AddIntrinsicEval(cx);
JS_AddIntrinsicJSON(cx);
JS_AddIntrinsicRegExp(cx);
JS_AddIntrinsicMapSet(cx);
JS_AddIntrinsicDate(cx);
}
// Creates a new JSContext with only the provided sandbox function
// in its global. Make sure to free the context when done with it.
//
static JSContext* make_sandbox(JSContext* cx, JSValue sbox) {
JSContext *cx1 = JS_NewContextRaw(JS_GetRuntime(cx));
if(!cx1) {
return NULL;
}
add_cx_methods(cx1);
JSValue global = JS_GetGlobalObject(cx1);
int i;
JSPropertyEnum *tab;
uint32_t tablen;
JSValue prop_val;
int prop_flags = JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY;
if(JS_GetOwnPropertyNames(cx, &tab, &tablen, sbox, prop_flags) < 0){
JS_FreeContext(cx1);
return NULL;
}
for(i=0; i < tablen; i++) {
prop_val = JS_GetProperty(cx, sbox, tab[i].atom);
if (JS_IsException(prop_val)) {
goto exception;
}
JS_SetProperty(cx1, global, tab[i].atom, prop_val);
}
for(i=0; i < tablen; i++) {
JS_FreeAtom(cx, tab[i].atom);
}
js_free(cx, tab);
JS_FreeValue(cx1, global);
return cx1;
exception:
for(i = 0; i < tablen; i++) {
JS_FreeAtom(cx, tab[i].atom);
}
js_free(cx, tab);
JS_FreeValue(cx1, global);
JS_FreeContext(cx1);
return NULL;
}
// This is mostly for test compatibility between engines, and
// some anti-footgun help for user code. For real sandboxing we rely on
// destroying and re-creating the whole JSRuntime instance.
//
static JSValue js_evalcx(JSContext* cx, JSValueConst this_val, int argc, JSValueConst *argv)
{
size_t strlen;
const char *str;
const char *name;
if (argc != 3) {
return JS_EXCEPTION;
}
if(!JS_IsObject(argv[1])) {
return JS_EXCEPTION;
}
JSValue sbox = argv[1];
str = JS_ToCStringLen(cx, &strlen, argv[0]);
if(!str) {
return JS_EXCEPTION;
}
name = JS_ToCString(cx, argv[2]);
if(!name) {
JS_FreeCString(cx, str);
return JS_EXCEPTION;
}
JSContext *cx1 = make_sandbox(cx, sbox);
if(!cx1) {
JS_FreeCString(cx, str);
JS_FreeCString(cx, name);
return JS_EXCEPTION;
}
int flags = JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_BACKTRACE_BARRIER;
JSValue res = JS_Eval(cx1, str, strlen, name, flags);
JS_FreeCString(cx, str);
JS_FreeCString(cx, name);
JS_FreeContext(cx1);
return res;
}
// Read \n terminated lines from stdin. *line must be malloc-ed buffer of size
// *linemax. Read stdin one character at a time and if needed will reallocate
// the *line buffer.
//
// On success the characters read will be in *line, and the return value will
// be the number character read including \n, but not including the final \0.
// Also, *linemax will contain the current, possible larger realloc-ed buffer
// size. On failure return -1.
//
// On most POSIX systems use a faster getline function, and on Windows use our
// own vendored copy, adapted from NetBSD tools/compat/getline.c
//
static ssize_t linein(char** line, size_t *linemax) {
#ifdef _WIN32
char *ptr, *eptr;
if (line == NULL || stdin == NULL || linemax == NULL) {
return -1;
}
if (*line == NULL || *linemax <= 1) {
return -1;
}
for (ptr = *line, eptr = *line + *linemax;;) {
int c = fgetc(stdin);
if (c == -1) {
if (feof(stdin)) {
ssize_t diff = (ssize_t)(ptr - *line);
if (diff != 0) {
*ptr = '\0';
return diff;
}
}
return -1;
}
*ptr++ = c;
if (c == '\n') {
*ptr = '\0';
return ptr - *line;
}
if (ptr + 1 > eptr) {
char *nline;
size_t nlinemax = *linemax * 2;
ssize_t d = ptr - *line;
if ((nline = realloc(*line, nlinemax)) == NULL) {
return -1;
}
*line = nline;
*linemax = nlinemax;
eptr = nline + nlinemax;
ptr = nline + d;
}
}
#else
return getline(line, linemax, stdin);
#endif
}
// Once list/show features are gone, could avoid this too and just have the
// dispatch return the list of rows as a response. That way JS can entirely
// avoid any IO logic, only take rows and return rows in a simple
// request/response manner.
//
static JSValue js_print(JSContext* cx, JSValueConst this_val, int argc, JSValueConst *argv)
{
const char *str;
if (argc == 1) {
str = JS_ToCString(cx, argv[0]);
if (!str) {
return JS_UNDEFINED;
}
fputs(str, stdout);
JS_FreeCString(cx, str);
} else if (argc > 1) {
return JS_EXCEPTION;
}
fputc('\n', stdout);
fflush(stdout);
return JS_UNDEFINED;
}
//TODO: remove when lists/show are gone. The only reason to have this function
//is to support getRow() for lists.
//
static JSValue js_readline(JSContext* cx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc != 0) return JS_EXCEPTION;
JSValue res;
size_t linemax = BUF_SIZE;
char* line = malloc(linemax);
if(!line) {
BAIL("Could not allocate line buffer for list sub-command");
}
int len;
if ((len = linein(&line, &linemax)) != -1) {
if (line[len - 1] != '\n') {
BAIL("list linein() didn't end in newline");
}
line[--len] = '\0'; // don't care about the last \n so shorten the string
switch (parse_command(line, len)) {
case CMD_LIST:
res = JS_NewStringLen(cx, (const char*)line, len);
break;
case CMD_EMPTY:
res = JS_NewString(cx, "");
break;
default:
BAIL("unexpected command during list subcommand mode");
}
free(line);
return res;
} else {
free(line);
return JS_EXCEPTION;
}
}
// TODO: This may not be neeed. Mainly for SM API compat to minimize main.js differences
//
static JSValue js_gc(JSContext* cx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc != 0) {
return JS_EXCEPTION;
}
JS_RunGC(JS_GetRuntime(cx));
return JS_UNDEFINED;
}
static void free_cx(JSContext* cx) {
if (cx == NULL) {
return;
}
JSRuntime* rt = JS_GetRuntime(cx);
if (rt == NULL) {
BAIL("JSRuntime is unexpectedly NULL");
}
JS_FreeContext(cx);
JS_FreeRuntime(rt);
}
static JSContext* new_cx(const couch_args* args) {
JSRuntime* rt;
JSContext* cx;
rt = JS_NewRuntime();
if (rt == NULL) {
BAIL("Could not create JSRuntime");
}
JS_SetMemoryLimit(rt, args->stack_size);
JS_SetMaxStackSize(rt, args->stack_size);
cx = JS_NewContextRaw(rt);
if (cx == NULL) {
BAIL("Could not create JSContext");
}
add_cx_methods(cx);
return cx;
}
// This is what we rely on for sandboxing. On a reset command, blow away
// the whole runtime instance and re-create it by re-evaluating the bytecode again
// in a new instance.
//
static JSContext* reset_cx(const couch_args* args, JSContext *cx) {
JSValue global, obj, val;
free_cx(cx);
cx = new_cx(args);
global = JS_GetGlobalObject(cx);
JS_SetPropertyStr(cx, global, "print", JS_NewCFunction(cx, js_print, "print", 1));
JS_SetPropertyStr(cx, global, "readline", JS_NewCFunction(cx, js_readline,"readline", 0));
JS_SetPropertyStr(cx, global, "gc", JS_NewCFunction(cx, js_gc, "gc", 0));
JS_SetPropertyStr(cx, global, "evalcx", JS_NewCFunction(cx, js_evalcx, "evalcx", 3));
obj = JS_ReadObject(cx, bytecode, bytecode_size, JS_READ_OBJ_BYTECODE);
if (JS_IsException(obj)) {
BAILJS(cx, "Error reading bytecode");
}
val = JS_EvalFunction(cx, obj); // this calls auto-frees obj
if (JS_IsException(val)) {
BAILJS(cx, "Error evaluating bytecode");
}
JS_FreeValue(cx, val);
JS_FreeValue(cx, global);
return cx;
}
// Dispatch a single command line to the engine. If it weren't for list functions we could have
// made it return the responses as a result too.
//
// The result is a boolean value indicating whether to continue processing or stop and exit.
//
static bool dispatch(JSContext* cx, char* str, size_t len) {
JSValue global = JS_GetGlobalObject(cx);
JSValue fun = JS_GetPropertyStr(cx, global, "dispatch");
if (JS_IsException(fun)) {
BAILJS(cx, "Could not find main dispatch function");
}
if (!JS_IsFunction(cx, fun)) {
BAIL("dispatch is not a function");
}
JSValue argv[] = {JS_NewStringLen(cx, str, len)};
JSValue jres = JS_Call(cx, fun, global, 1, argv);
if (JS_IsException(jres)) {
BAILJS(cx, "couchjs internal error");
}
if (!JS_IsBool(jres)) {
BAIL("dispatch didn't return boolean value");
}
bool res = JS_VALUE_GET_BOOL(jres);
JS_FreeValue(cx, jres);
JS_FreeValue(cx, argv[0]);
JS_FreeValue(cx, fun);
JS_FreeValue(cx, global);
return res;
}
int main(int argc, const char* argv[])
{
JSContext* view_cx = NULL;
JSContext* ddoc_cx = NULL;
couch_args args = {.stack_size = DEFAULT_STACK_SIZE};
parse_args(argc, argv, &args);
//load_bytecode(&args);
size_t linemax = BUF_SIZE;
char* line = malloc(linemax);
if (!line) {
BAIL("Could not allocate line buffer");
}
int len;
bool do_continue = true;
while (do_continue && (len = linein(&line, &linemax)) != -1) {
if (line[len - 1] != '\n') {
BAIL("linein() didn't end in newline");
}
line[--len] = '\0'; // don't care about the last \n so shorten the string
switch (parse_command(line, len)) {
case CMD_RESET:
view_cx = reset_cx(&args, view_cx);
do_continue = dispatch(view_cx, line, len);
break;
case CMD_DDOC:
if (ddoc_cx == NULL) {
ddoc_cx = reset_cx(&args, NULL);
}
do_continue = dispatch(ddoc_cx, line, len);
break;
case CMD_VIEW:
if (view_cx == NULL) {
view_cx = reset_cx(&args, NULL);
}
do_continue = dispatch(view_cx, line, len);
break;
case CMD_EMPTY:
do_continue = false;
break;
case CMD_LIST:
BAIL("unexpected list subcommand in the main command loop");
}
}
free_cx(view_cx);
free_cx(ddoc_cx);
free(line);
return EXIT_SUCCESS;
}