You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by wo...@apache.org on 2020/04/27 17:31:17 UTC

[couchdb] 01/03: Fix couchjs utf8 conversions (#2786)

This is an automated email from the ASF dual-hosted git repository.

wohali pushed a commit to branch 3.0.x-backports
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit c491c9a8b2b8d5405f40893ce771ddb44ddac7de
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Fri Apr 17 20:12:25 2020 -0500

    Fix couchjs utf8 conversions (#2786)
    
    * Remove unused string conversion functions
    * Set UTF-8 encoding when compiling scripts
    * Encode JavaScript strings as UTF-8 for printing
    * Check that only strings are passed to print
    * Use builtin UTF-8 conversions in http.cpp
    * Add tests for couchjs UTF-8 support
    * Remove custom UTF-8 conversion functions
    
    We're now using 100% built-in functionality of SpiderMonkey to handle
    all UTF-8 conversions.
    
    * Report error messages at global scope
    
    Previously we weren't reporting any uncaught exceptions or compilation
    errors. This changes that to print any compilation errors or any
    uncaught exceptions with stack traces.
    
    The previous implementation of `couch_error` was attempting to call
    `String.replace` on the `stack` member string of the thrown exception.
    This likely never worked and attempting to fix I was unable to properly
    invoke the `String.replace` function. This changes the implementation to
    use the builtin stack formatting method instead.
    
    * Modernize sources to minimize changes for 68
    
    These are a handful of changes that modernize various aspects of the
    couchjs 60 source files. Behaviorally they're all benign but will
    shorten the diff required for adding support for SpiderMonkey 68.
    
    Co-authored-by: Joan Touzet <wo...@apache.org>
---
 src/couch/priv/couch_js/60/http.cpp     | 214 +++++++++--------------
 src/couch/priv/couch_js/60/main.cpp     |  69 ++++++--
 src/couch/priv/couch_js/60/utf8.cpp     | 301 --------------------------------
 src/couch/priv/couch_js/60/utf8.h       |  19 --
 src/couch/priv/couch_js/60/util.cpp     | 196 ++++++++++++---------
 src/couch/priv/couch_js/60/util.h       |   4 +-
 src/couch/test/eunit/couch_js_tests.erl | 140 +++++++++++++--
 7 files changed, 374 insertions(+), 569 deletions(-)

diff --git a/src/couch/priv/couch_js/60/http.cpp b/src/couch/priv/couch_js/60/http.cpp
index 9ab47b2..e1e44d6 100644
--- a/src/couch/priv/couch_js/60/http.cpp
+++ b/src/couch/priv/couch_js/60/http.cpp
@@ -18,7 +18,6 @@
 #include <jsapi.h>
 #include <js/Initialization.h>
 #include "config.h"
-#include "utf8.h"
 #include "util.h"
 
 // Soft dependency on cURL bindings because they're
@@ -100,7 +99,6 @@ http_check_enabled()
 #ifdef XP_WIN
 #define strcasecmp _strcmpi
 #define strncasecmp _strnicmp
-#define snprintf _snprintf
 #endif
 
 
@@ -109,7 +107,7 @@ typedef struct curl_slist CurlHeaders;
 
 typedef struct {
     int             method;
-    char*           url;
+    std::string     url;
     CurlHeaders*    req_headers;
     int16_t          last_status;
 } HTTPData;
@@ -127,21 +125,15 @@ const char* METHODS[] = {"GET", "HEAD", "POST", "PUT", "DELETE", "COPY", "OPTION
 #define OPTIONS 6
 
 
-static bool
-go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t blen);
-
-
-static JSString*
-str_from_binary(JSContext* cx, char* data, size_t length);
+static bool go(JSContext* cx, JSObject* obj, HTTPData* http, std::string& body);
 
 
 bool
 http_ctor(JSContext* cx, JSObject* req)
 {
-    HTTPData* http = NULL;
+    HTTPData* http = new HTTPData();
     bool ret = false;
 
-    http = (HTTPData*) malloc(sizeof(HTTPData));
     if(!http)
     {
         JS_ReportErrorUTF8(cx, "Failed to create CouchHTTP instance.");
@@ -149,7 +141,6 @@ http_ctor(JSContext* cx, JSObject* req)
     }
 
     http->method = -1;
-    http->url = NULL;
     http->req_headers = NULL;
     http->last_status = -1;
 
@@ -159,7 +150,7 @@ http_ctor(JSContext* cx, JSObject* req)
     goto success;
 
 error:
-    if(http) free(http);
+    if(http) delete http;
 
 success:
     return ret;
@@ -171,9 +162,8 @@ http_dtor(JSFreeOp* fop, JSObject* obj)
 {
     HTTPData* http = (HTTPData*) JS_GetPrivate(obj);
     if(http) {
-        if(http->url) free(http->url);
         if(http->req_headers) curl_slist_free_all(http->req_headers);
-        free(http);
+        delete http;
     }
 }
 
@@ -182,56 +172,50 @@ bool
 http_open(JSContext* cx, JSObject* req, JS::Value mth, JS::Value url, JS::Value snc)
 {
     HTTPData* http = (HTTPData*) JS_GetPrivate(req);
-    char* method = NULL;
     int methid;
-    bool ret = false;
 
     if(!http) {
         JS_ReportErrorUTF8(cx, "Invalid CouchHTTP instance.");
-        goto done;
+        return false;
     }
 
-    if(mth.isUndefined()) {
-        JS_ReportErrorUTF8(cx, "You must specify a method.");
-        goto done;
+    if(!mth.isString()) {
+        JS_ReportErrorUTF8(cx, "Method must be a string.");
+        return false;
     }
 
-    method = enc_string(cx, mth, NULL);
-    if(!method) {
+    std::string method;
+    if(!js_to_string(cx, JS::RootedValue(cx, mth), method)) {
         JS_ReportErrorUTF8(cx, "Failed to encode method.");
-        goto done;
+        return false;
     }
 
     for(methid = 0; METHODS[methid] != NULL; methid++) {
-        if(strcasecmp(METHODS[methid], method) == 0) break;
+        if(strcasecmp(METHODS[methid], method.c_str()) == 0) break;
     }
 
     if(methid > OPTIONS) {
         JS_ReportErrorUTF8(cx, "Invalid method specified.");
-        goto done;
+        return false;
     }
 
     http->method = methid;
 
-    if(url.isUndefined()) {
-        JS_ReportErrorUTF8(cx, "You must specify a URL.");
-        goto done;
-    }
-
-    if(http->url != NULL) {
-        free(http->url);
-        http->url = NULL;
+    if(!url.isString()) {
+        JS_ReportErrorUTF8(cx, "URL must be a string");
+        return false;
     }
 
-    http->url = enc_string(cx, url, NULL);
-    if(http->url == NULL) {
+    std::string urlstr;
+    if(!js_to_string(cx, JS::RootedValue(cx, url), urlstr)) {
         JS_ReportErrorUTF8(cx, "Failed to encode URL.");
-        goto done;
+        return false;
     }
+    http->url = urlstr;
 
     if(snc.isBoolean() && snc.isTrue()) {
         JS_ReportErrorUTF8(cx, "Synchronous flag must be false.");
-        goto done;
+        return false;
     }
 
     if(http->req_headers) {
@@ -242,11 +226,7 @@ http_open(JSContext* cx, JSObject* req, JS::Value mth, JS::Value url, JS::Value
     // Disable Expect: 100-continue
     http->req_headers = curl_slist_append(http->req_headers, "Expect:");
 
-    ret = true;
-
-done:
-    if(method) free(method);
-    return ret;
+    return true;
 }
 
 
@@ -254,88 +234,60 @@ bool
 http_set_hdr(JSContext* cx, JSObject* req, JS::Value name, JS::Value val)
 {
     HTTPData* http = (HTTPData*) JS_GetPrivate(req);
-    char* keystr = NULL;
-    char* valstr = NULL;
-    char* hdrbuf = NULL;
-    size_t hdrlen = -1;
-    bool ret = false;
 
     if(!http) {
         JS_ReportErrorUTF8(cx, "Invalid CouchHTTP instance.");
-        goto done;
+        return false;
     }
 
-    if(name.isUndefined())
+    if(!name.isString())
     {
-        JS_ReportErrorUTF8(cx, "You must speciy a header name.");
-        goto done;
+        JS_ReportErrorUTF8(cx, "Header names must be strings.");
+        return false;
     }
 
-    keystr = enc_string(cx, name, NULL);
-    if(!keystr)
+    std::string keystr;
+    if(!js_to_string(cx, JS::RootedValue(cx, name), keystr))
     {
         JS_ReportErrorUTF8(cx, "Failed to encode header name.");
-        goto done;
+        return false;
     }
 
-    if(val.isUndefined())
+    if(!val.isString())
     {
-        JS_ReportErrorUTF8(cx, "You must specify a header value.");
-        goto done;
+        JS_ReportErrorUTF8(cx, "Header values must be strings.");
+        return false;
     }
 
-    valstr = enc_string(cx, val, NULL);
-    if(!valstr)
-    {
+    std::string valstr;
+    if(!js_to_string(cx, JS::RootedValue(cx, val), valstr)) {
         JS_ReportErrorUTF8(cx, "Failed to encode header value.");
-        goto done;
-    }
-
-    hdrlen = strlen(keystr) + strlen(valstr) + 3;
-    hdrbuf = (char*) malloc(hdrlen * sizeof(char));
-    if(!hdrbuf) {
-        JS_ReportErrorUTF8(cx, "Failed to allocate header buffer.");
-        goto done;
+        return false;
     }
 
-    snprintf(hdrbuf, hdrlen, "%s: %s", keystr, valstr);
-    http->req_headers = curl_slist_append(http->req_headers, hdrbuf);
-
-    ret = true;
+    std::string header = keystr + ": " + valstr;
+    http->req_headers = curl_slist_append(http->req_headers, header.c_str());
 
-done:
-    if(keystr) free(keystr);
-    if(valstr) free(valstr);
-    if(hdrbuf) free(hdrbuf);
-    return ret;
+    return true;
 }
 
 bool
 http_send(JSContext* cx, JSObject* req, JS::Value body)
 {
     HTTPData* http = (HTTPData*) JS_GetPrivate(req);
-    char* bodystr = NULL;
-    size_t bodylen = 0;
-    bool ret = false;
 
     if(!http) {
         JS_ReportErrorUTF8(cx, "Invalid CouchHTTP instance.");
-        goto done;
+        return false;
     }
 
-    if(!body.isUndefined()) {
-        bodystr = enc_string(cx, body, &bodylen);
-        if(!bodystr) {
-            JS_ReportErrorUTF8(cx, "Failed to encode body.");
-            goto done;
-        }
+    std::string bodystr;
+    if(!js_to_string(cx, JS::RootedValue(cx, body), bodystr)) {
+        JS_ReportErrorUTF8(cx, "Failed to encode body.");
+        return false;
     }
 
-    ret = go(cx, req, http, bodystr, bodylen);
-
-done:
-    if(bodystr) free(bodystr);
-    return ret;
+    return go(cx, req, http, bodystr);
 }
 
 int
@@ -395,7 +347,7 @@ typedef struct {
     HTTPData*   http;
     JSContext*  cx;
     JSObject*   resp_headers;
-    char*       sendbuf;
+    const char* sendbuf;
     size_t      sendlen;
     size_t      sent;
     int         sent_once;
@@ -417,10 +369,9 @@ static size_t recv_body(void *ptr, size_t size, size_t nmem, void *data);
 static size_t recv_header(void *ptr, size_t size, size_t nmem, void *data);
 
 static bool
-go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen)
+go(JSContext* cx, JSObject* obj, HTTPData* http, std::string& body)
 {
     CurlState state;
-    char* referer;
     JSString* jsbody;
     bool ret = false;
     JS::Value tmp;
@@ -431,8 +382,8 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen)
     state.cx = cx;
     state.http = http;
 
-    state.sendbuf = body;
-    state.sendlen = bodylen;
+    state.sendbuf = body.c_str();
+    state.sendlen = body.size();
     state.sent = 0;
     state.sent_once = 0;
 
@@ -463,13 +414,13 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen)
 
     tmp = JS_GetReservedSlot(obj, 0);
 
-    if(!(referer = enc_string(cx, tmp, NULL))) {
+    std::string referer;
+    if(!js_to_string(cx, JS::RootedValue(cx, tmp), referer)) {
         JS_ReportErrorUTF8(cx, "Failed to encode referer.");
         if(state.recvbuf) JS_free(cx, state.recvbuf);
-          return ret;
+        return ret;
     }
-    curl_easy_setopt(HTTP_HANDLE, CURLOPT_REFERER, referer);
-    free(referer);
+    curl_easy_setopt(HTTP_HANDLE, CURLOPT_REFERER, referer.c_str());
 
     if(http->method < 0 || http->method > OPTIONS) {
         JS_ReportErrorUTF8(cx, "INTERNAL: Unknown method.");
@@ -490,15 +441,15 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen)
         curl_easy_setopt(HTTP_HANDLE, CURLOPT_FOLLOWLOCATION, 0);
     }
 
-    if(body && bodylen) {
-        curl_easy_setopt(HTTP_HANDLE, CURLOPT_INFILESIZE, bodylen);
+    if(body.size() > 0) {
+        curl_easy_setopt(HTTP_HANDLE, CURLOPT_INFILESIZE, body.size());
     } else {
         curl_easy_setopt(HTTP_HANDLE, CURLOPT_INFILESIZE, 0);
     }
 
     // curl_easy_setopt(HTTP_HANDLE, CURLOPT_VERBOSE, 1);
 
-    curl_easy_setopt(HTTP_HANDLE, CURLOPT_URL, http->url);
+    curl_easy_setopt(HTTP_HANDLE, CURLOPT_URL, http->url.c_str());
     curl_easy_setopt(HTTP_HANDLE, CURLOPT_HTTPHEADER, http->req_headers);
     curl_easy_setopt(HTTP_HANDLE, CURLOPT_READDATA, &state);
     curl_easy_setopt(HTTP_HANDLE, CURLOPT_SEEKDATA, &state);
@@ -532,12 +483,13 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen)
 
     if(state.recvbuf) {
         state.recvbuf[state.read] = '\0';
-        jsbody = dec_string(cx, state.recvbuf, state.read+1);
+        std::string bodystr(state.recvbuf, state.read);
+        jsbody = string_to_js(cx, bodystr);
         if(!jsbody) {
             // If we can't decode the body as UTF-8 we forcefully
             // convert it to a string by just forcing each byte
             // to a char16_t.
-            jsbody = str_from_binary(cx, state.recvbuf, state.read);
+            jsbody = JS_NewStringCopyN(cx, state.recvbuf, state.read);
             if(!jsbody) {
                 if(!JS_IsExceptionPending(cx)) {
                     JS_ReportErrorUTF8(cx, "INTERNAL: Failed to decode body.");
@@ -572,7 +524,7 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen)
 static size_t
 send_body(void *ptr, size_t size, size_t nmem, void *data)
 {
-    CurlState* state = (CurlState*) data;
+    CurlState* state = static_cast<CurlState*>(data);
     size_t length = size * nmem;
     size_t towrite = state->sendlen - state->sent;
 
@@ -598,19 +550,19 @@ send_body(void *ptr, size_t size, size_t nmem, void *data)
 static int
 seek_body(void* ptr, curl_off_t offset, int origin)
 {
-    CurlState* state = (CurlState*) ptr;
+    CurlState* state = static_cast<CurlState*>(ptr);
     if(origin != SEEK_SET) return -1;
 
-    state->sent = (size_t) offset;
-    return (int) state->sent;
+    state->sent = static_cast<size_t>(offset);
+    return static_cast<int>(state->sent);
 }
 
 static size_t
 recv_header(void *ptr, size_t size, size_t nmem, void *data)
 {
-    CurlState* state = (CurlState*) data;
+    CurlState* state = static_cast<CurlState*>(data);
     char code[4];
-    char* header = (char*) ptr;
+    char* header = static_cast<char*>(ptr);
     size_t length = size * nmem;
     JSString* hdr = NULL;
     uint32_t hdrlen;
@@ -638,7 +590,8 @@ recv_header(void *ptr, size_t size, size_t nmem, void *data)
     }
 
     // Append the new header to our array.
-    hdr = dec_string(state->cx, header, length);
+    std::string hdrstr(header, length);
+    hdr = string_to_js(state->cx, hdrstr);
     if(!hdr) {
         return CURLE_WRITE_ERROR;
     }
@@ -659,14 +612,17 @@ recv_header(void *ptr, size_t size, size_t nmem, void *data)
 static size_t
 recv_body(void *ptr, size_t size, size_t nmem, void *data)
 {
-    CurlState* state = (CurlState*) data;
+    CurlState* state = static_cast<CurlState*>(data);
     size_t length = size * nmem;
     char* tmp = NULL;
 
     if(!state->recvbuf) {
         state->recvlen = 4096;
         state->read = 0;
-        state->recvbuf = (char *)JS_malloc(state->cx, state->recvlen);
+        state->recvbuf = static_cast<char*>(JS_malloc(
+                state->cx,
+                state->recvlen
+            ));
     }
 
     if(!state->recvbuf) {
@@ -676,7 +632,12 @@ recv_body(void *ptr, size_t size, size_t nmem, void *data)
     // +1 so we can add '\0' back up in the go function.
     size_t oldlen = state->recvlen;
     while(length+1 > state->recvlen - state->read) state->recvlen *= 2;
-    tmp = (char *) JS_realloc(state->cx, state->recvbuf, oldlen, state->recvlen);
+    tmp = static_cast<char*>(JS_realloc(
+            state->cx,
+            state->recvbuf,
+            oldlen,
+            state->recvlen
+        ));
     if(!tmp) return CURLE_WRITE_ERROR;
     state->recvbuf = tmp;
 
@@ -685,23 +646,4 @@ recv_body(void *ptr, size_t size, size_t nmem, void *data)
     return length;
 }
 
-JSString*
-str_from_binary(JSContext* cx, char* data, size_t length)
-{
-    char16_t* conv = (char16_t*) JS_malloc(cx, length * sizeof(char16_t));
-    JSString* ret = NULL;
-    size_t i;
-
-    if(!conv) return NULL;
-
-    for(i = 0; i < length; i++) {
-        conv[i] = (char16_t) data[i];
-    }
-
-    ret = JS_NewUCString(cx, conv, length);
-    if(!ret) JS_free(cx, conv);
-
-    return ret;
-}
-
 #endif /* HAVE_CURL */
diff --git a/src/couch/priv/couch_js/60/main.cpp b/src/couch/priv/couch_js/60/main.cpp
index b6157ed..828b9da 100644
--- a/src/couch/priv/couch_js/60/main.cpp
+++ b/src/couch/priv/couch_js/60/main.cpp
@@ -28,7 +28,6 @@
 
 #include "config.h"
 #include "http.h"
-#include "utf8.h"
 #include "util.h"
 
 static bool enableSharedMemory = true;
@@ -99,8 +98,9 @@ req_ctor(JSContext* cx, unsigned int argc, JS::Value* vp)
 static bool
 req_open(JSContext* cx, unsigned int argc, JS::Value* vp)
 {
-    JSObject* obj = JS_THIS_OBJECT(cx, vp);
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JS::Value vobj = args.computeThis(cx);
+    JSObject* obj = vobj.toObjectOrNull();
     bool ret = false;
 
     if(argc == 2) {
@@ -119,8 +119,9 @@ req_open(JSContext* cx, unsigned int argc, JS::Value* vp)
 static bool
 req_set_hdr(JSContext* cx, unsigned int argc, JS::Value* vp)
 {
-    JSObject* obj = JS_THIS_OBJECT(cx, vp);
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JS::Value vobj = args.computeThis(cx);
+    JSObject* obj = vobj.toObjectOrNull();
     bool ret = false;
 
     if(argc == 2) {
@@ -137,8 +138,9 @@ req_set_hdr(JSContext* cx, unsigned int argc, JS::Value* vp)
 static bool
 req_send(JSContext* cx, unsigned int argc, JS::Value* vp)
 {
-    JSObject* obj = JS_THIS_OBJECT(cx, vp);
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JS::Value vobj = args.computeThis(cx);
+    JSObject* obj = vobj.toObjectOrNull();
     bool ret = false;
 
     if(argc == 1) {
@@ -155,7 +157,9 @@ static bool
 req_status(JSContext* cx, unsigned int argc, JS::Value* vp)
 {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
-    JSObject* obj = JS_THIS_OBJECT(cx, vp);
+    JS::Value vobj = args.computeThis(cx);
+    JSObject* obj = vobj.toObjectOrNull();
+
     int status = http_status(cx, obj);
 
     if(status < 0)
@@ -169,8 +173,10 @@ static bool
 base_url(JSContext *cx, unsigned int argc, JS::Value* vp)
 {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
-    JSObject* obj = JS_THIS_OBJECT(cx, vp);
-    couch_args *cargs = (couch_args*)JS_GetContextPrivate(cx);
+    JS::Value vobj = args.computeThis(cx);
+    JSObject* obj = vobj.toObjectOrNull();
+
+    couch_args *cargs = static_cast<couch_args*>(JS_GetContextPrivate(cx));
     JS::Value uri_val;
     bool rc = http_uri(cx, obj, cargs, &uri_val);
     args.rval().set(uri_val);
@@ -226,9 +232,15 @@ evalcx(JSContext *cx, unsigned int argc, JS::Value* vp)
         if (!sandbox)
             return false;
     }
-    JS_BeginRequest(cx);
+
     JSAutoRequest ar(cx);
 
+    if (!sandbox) {
+        sandbox = NewSandbox(cx, false);
+        if (!sandbox)
+            return false;
+    }
+
     js::AutoStableStringChars strChars(cx);
     if (!strChars.initTwoByte(cx, str))
         return false;
@@ -237,12 +249,6 @@ evalcx(JSContext *cx, unsigned int argc, JS::Value* vp)
     size_t srclen = chars.length();
     const char16_t* src = chars.begin().get();
 
-    if (!sandbox) {
-        sandbox = NewSandbox(cx, false);
-        if (!sandbox)
-            return false;
-    }
-
     if(srclen == 0) {
         args.rval().setObject(*sandbox);
     } else {
@@ -283,7 +289,19 @@ static bool
 print(JSContext* cx, unsigned int argc, JS::Value* vp)
 {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
-    couch_print(cx, argc, args);
+
+    bool use_stderr = false;
+    if(argc > 1 && args[1].isTrue()) {
+        use_stderr = true;
+    }
+
+    if(!args[0].isString()) {
+        JS_ReportErrorUTF8(cx, "Unable to print non-string value.");
+        return false;
+    }
+
+    couch_print(cx, args[0], use_stderr);
+
     args.rval().setUndefined();
     return true;
 }
@@ -386,7 +404,7 @@ static JSFunctionSpec global_functions[] = {
 static bool
 csp_allows(JSContext* cx)
 {
-    couch_args *args = (couch_args*)JS_GetContextPrivate(cx);
+    couch_args* args = static_cast<couch_args*>(JS_GetContextPrivate(cx));
     if(args->eval) {
         return true;
     } else {
@@ -473,10 +491,18 @@ main(int argc, const char* argv[])
         // Compile and run
         JS::CompileOptions options(cx);
         options.setFileAndLine(args->scripts[i], 1);
+        options.setUTF8(true);
         JS::RootedScript script(cx);
 
         if(!JS_CompileScript(cx, scriptsrc, slen, options, &script)) {
-            fprintf(stderr, "Failed to compile script.\n");
+            JS::RootedValue exc(cx);
+            if(!JS_GetPendingException(cx, &exc)) {
+                fprintf(stderr, "Failed to compile script.\n");
+            } else {
+                JS::RootedObject exc_obj(cx, &exc.toObject());
+                JSErrorReport* report = JS_ErrorFromException(cx, exc_obj);
+                couch_error(cx, report);
+            }
             return 1;
         }
 
@@ -484,7 +510,14 @@ main(int argc, const char* argv[])
 
         JS::RootedValue result(cx);
         if(JS_ExecuteScript(cx, script, &result) != true) {
-            fprintf(stderr, "Failed to execute script.\n");
+            JS::RootedValue exc(cx);
+            if(!JS_GetPendingException(cx, &exc)) {
+                fprintf(stderr, "Failed to execute script.\n");
+            } else {
+                JS::RootedObject exc_obj(cx, &exc.toObject());
+                JSErrorReport* report = JS_ErrorFromException(cx, exc_obj);
+                couch_error(cx, report);
+            }
             return 1;
         }
 
diff --git a/src/couch/priv/couch_js/60/utf8.cpp b/src/couch/priv/couch_js/60/utf8.cpp
deleted file mode 100644
index 38dfa62..0000000
--- a/src/couch/priv/couch_js/60/utf8.cpp
+++ /dev/null
@@ -1,301 +0,0 @@
-// 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 <jsapi.h>
-#include <js/Initialization.h>
-#include <js/Conversions.h>
-#include <js/Wrapper.h>
-#include "config.h"
-#include "util.h"
-
-static int
-enc_char(uint8_t *utf8Buffer, uint32_t ucs4Char)
-{
-    int utf8Length = 1;
-
-    if (ucs4Char < 0x80)
-    {
-        *utf8Buffer = (uint8_t)ucs4Char;
-    }
-    else
-    {
-        int i;
-        uint32_t a = ucs4Char >> 11;
-        utf8Length = 2;
-        while(a)
-        {
-            a >>= 5;
-            utf8Length++;
-        }
-        i = utf8Length;
-        while(--i)
-        {
-            utf8Buffer[i] = (uint8_t)((ucs4Char & 0x3F) | 0x80);
-            ucs4Char >>= 6;
-        }
-        *utf8Buffer = (uint8_t)(0x100 - (1 << (8-utf8Length)) + ucs4Char);
-    }
-
-    return utf8Length;
-}
-
-static bool
-enc_charbuf(const char16_t* src, size_t srclen, char* dst, size_t* dstlenp)
-{
-    size_t i;
-    size_t utf8Len;
-    size_t dstlen = *dstlenp;
-    size_t origDstlen = dstlen;
-    char16_t c;
-    char16_t c2;
-    uint32_t v;
-    uint8_t utf8buf[6];
-
-    if(!dst)
-    {
-        dstlen = origDstlen = (size_t) -1;
-    }
-
-    while(srclen)
-    {
-        c = *src++;
-        srclen--;
-
-        if(c <= 0xD7FF || c >= 0xE000)
-        {
-            v = (uint32_t) c;
-        }
-        else if(c >= 0xD800 && c <= 0xDBFF)
-        {
-            if(srclen < 1) goto buffer_too_small;
-            c2 = *src++;
-            srclen--;
-            if(c2 >= 0xDC00 && c2 <= 0xDFFF)
-            {
-                v = (uint32_t) (((c - 0xD800) << 10) + (c2 - 0xDC00) + 0x10000);
-            }
-            else
-            {
-                // Invalid second half of surrogate pair
-                v = (uint32_t) 0xFFFD;
-                // Undo our character advancement
-                src--;
-                srclen++;
-            }
-        }
-        else
-        {
-            // Invalid first half surrogate pair
-            v = (uint32_t) 0xFFFD;
-        }
-
-        if(v < 0x0080)
-        {
-            /* no encoding necessary - performance hack */
-            if(!dstlen) goto buffer_too_small;
-            if(dst) *dst++ = (char) v;
-            utf8Len = 1;
-        }
-        else
-        {
-            utf8Len = enc_char(utf8buf, v);
-            if(utf8Len > dstlen) goto buffer_too_small;
-            if(dst)
-            {
-                for (i = 0; i < utf8Len; i++)
-                {
-                    *dst++ = (char) utf8buf[i];
-                }
-            }
-        }
-        dstlen -= utf8Len;
-    }
-    
-    *dstlenp = (origDstlen - dstlen);
-    return true;
-
-buffer_too_small:
-    *dstlenp = (origDstlen - dstlen);
-    return false;
-}
-
-char*
-enc_string(JSContext* cx, JS::Value arg, size_t* buflen)
-{
-    JSString* str = NULL;
-    const char16_t* src = NULL;
-    char* bytes = NULL;
-    size_t srclen = 0;
-    size_t byteslen = 0;
-    js::AutoStableStringChars rawChars(cx);
-    
-    str = arg.toString();
-    if(!str) goto error;
-
-    if (!rawChars.initTwoByte(cx, str))
-        return NULL;
-
-    src = rawChars.twoByteRange().begin().get();
-    srclen = JS_GetStringLength(str);
-
-    if(!enc_charbuf(src, srclen, NULL, &byteslen)) goto error;
-    
-    bytes = (char *)JS_malloc(cx, (byteslen) + 1);
-    bytes[byteslen] = 0;
-    
-    if(!enc_charbuf(src, srclen, bytes, &byteslen)) goto error;
-
-    if(buflen) *buflen = byteslen;
-    goto success;
-
-error:
-    if(bytes != NULL) JS_free(cx, bytes);
-    bytes = NULL;
-
-success:
-    return bytes;
-}
-
-static uint32_t
-dec_char(const uint8_t *utf8Buffer, int utf8Length)
-{
-    uint32_t ucs4Char;
-    uint32_t minucs4Char;
-
-    /* from Unicode 3.1, non-shortest form is illegal */
-    static const uint32_t minucs4Table[] = {
-        0x00000080, 0x00000800, 0x0001000, 0x0020000, 0x0400000
-    };
-
-    if (utf8Length == 1)
-    {
-        ucs4Char = *utf8Buffer;
-    }
-    else
-    {
-        ucs4Char = *utf8Buffer++ & ((1<<(7-utf8Length))-1);
-        minucs4Char = minucs4Table[utf8Length-2];
-        while(--utf8Length)
-        {
-            ucs4Char = ucs4Char<<6 | (*utf8Buffer++ & 0x3F);
-        }
-        if(ucs4Char < minucs4Char || ucs4Char == 0xFFFE || ucs4Char == 0xFFFF)
-        {
-            ucs4Char = 0xFFFD;
-        }
-    }
-
-    return ucs4Char;
-}
-
-static bool
-dec_charbuf(const char *src, size_t srclen, char16_t *dst, size_t *dstlenp)
-{
-    uint32_t v;
-    size_t offset = 0;
-    size_t j;
-    size_t n;
-    size_t dstlen = *dstlenp;
-    size_t origDstlen = dstlen;
-
-    if(!dst) dstlen = origDstlen = (size_t) -1;
-
-    while(srclen)
-    {
-        v = (uint8_t) *src;
-        n = 1;
-        
-        if(v & 0x80)
-        {
-            while(v & (0x80 >> n))
-            {
-                n++;
-            }
-            
-            if(n > srclen) goto buffer_too_small;
-            if(n == 1 || n > 6) goto bad_character;
-            
-            for(j = 1; j < n; j++)
-            {
-                if((src[j] & 0xC0) != 0x80) goto bad_character;
-            }
-
-            v = dec_char((const uint8_t *) src, n);
-            if(v >= 0x10000)
-            {
-                v -= 0x10000;
-                
-                if(v > 0xFFFFF || dstlen < 2)
-                {
-                    *dstlenp = (origDstlen - dstlen);
-                    return false;
-                }
-                
-                if(dstlen < 2) goto buffer_too_small;
-
-                if(dst)
-                {
-                    *dst++ = (char16_t)((v >> 10) + 0xD800);
-                    v = (char16_t)((v & 0x3FF) + 0xDC00);
-                }
-                dstlen--;
-            }
-        }
-
-        if(!dstlen) goto buffer_too_small;
-        if(dst) *dst++ = (char16_t) v;
-
-        dstlen--;
-        offset += n;
-        src += n;
-        srclen -= n;
-    }
-
-    *dstlenp = (origDstlen - dstlen);
-    return true;
-
-bad_character:
-    *dstlenp = (origDstlen - dstlen);
-    return false;
-
-buffer_too_small:
-    *dstlenp = (origDstlen - dstlen);
-    return false;
-}
-
-JSString*
-dec_string(JSContext* cx, const char* bytes, size_t byteslen)
-{
-    JSString* str = NULL;
-    char16_t* chars = NULL;
-    size_t charslen;
-    
-    if(!dec_charbuf(bytes, byteslen, NULL, &charslen)) goto error;
-
-    chars = (char16_t *)JS_malloc(cx, (charslen + 1) * sizeof(char16_t));
-    if(!chars) return NULL;
-    chars[charslen] = 0;
-
-    if(!dec_charbuf(bytes, byteslen, chars, &charslen)) goto error;
-
-    str = JS_NewUCString(cx, chars, charslen - 1);
-    if(!str) goto error;
-
-    goto success;
-
-error:
-    if(chars != NULL) JS_free(cx, chars);
-    str = NULL;
-
-success:
-    return str;
-}
diff --git a/src/couch/priv/couch_js/60/utf8.h b/src/couch/priv/couch_js/60/utf8.h
deleted file mode 100644
index c8b1f4d..0000000
--- a/src/couch/priv/couch_js/60/utf8.h
+++ /dev/null
@@ -1,19 +0,0 @@
-// 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.
-
-#ifndef COUCH_JS_UTF_8_H
-#define COUCH_JS_UTF_8_H
-
-char* enc_string(JSContext* cx, JS::Value arg, size_t* buflen);
-JSString* dec_string(JSContext* cx, const char* buf, size_t buflen);
-
-#endif
diff --git a/src/couch/priv/couch_js/60/util.cpp b/src/couch/priv/couch_js/60/util.cpp
index 92c6cbf..c37c41f 100644
--- a/src/couch/priv/couch_js/60/util.cpp
+++ b/src/couch/priv/couch_js/60/util.cpp
@@ -13,53 +13,76 @@
 #include <stdlib.h>
 #include <string.h>
 
+#include <sstream>
+
 #include <jsapi.h>
 #include <js/Initialization.h>
+#include <js/CharacterEncoding.h>
 #include <js/Conversions.h>
+#include <mozilla/Unused.h>
 
 #include "help.h"
 #include "util.h"
-#include "utf8.h"
 
 std::string
 js_to_string(JSContext* cx, JS::HandleValue val)
 {
+    JS::AutoSaveExceptionState exc_state(cx);
     JS::RootedString sval(cx);
     sval = val.toString();
 
     JS::UniqueChars chars(JS_EncodeStringToUTF8(cx, sval));
     if(!chars) {
         JS_ClearPendingException(cx);
-        fprintf(stderr, "Error converting value to string.\n");
-        exit(3);
+        return std::string();
     }
 
     return chars.get();
 }
 
-std::string
-js_to_string(JSContext* cx, JSString *str)
+bool
+js_to_string(JSContext* cx, JS::HandleValue val, std::string& str)
 {
-    JS::UniqueChars chars(JS_EncodeString(cx, str));
-    if(!chars) {
-        JS_ClearPendingException(cx);
-        fprintf(stderr, "Error converting  to string.\n");
-        exit(3);
+    if(!val.isString()) {
+        return false;
     }
 
-    return chars.get();
+    if(JS_GetStringLength(val.toString()) == 0) {
+        str = "";
+        return true;
+    }
+
+    std::string conv = js_to_string(cx, val);
+    if(!conv.size()) {
+        return false;
+    }
+
+    str = conv;
+    return true;
 }
 
 JSString*
-string_to_js(JSContext* cx, const std::string& s)
+string_to_js(JSContext* cx, const std::string& raw)
 {
-    JSString* ret = JS_NewStringCopyN(cx, s.c_str(), s.size());
-    if(ret != nullptr) {
-        return ret;
+    JS::UTF8Chars utf8(raw.c_str(), raw.size());
+    JS::UniqueTwoByteChars utf16;
+    size_t len;
+
+    utf16.reset(JS::UTF8CharsToNewTwoByteCharsZ(cx, utf8, &len).get());
+    if(!utf16) {
+        return nullptr;
+    }
+
+    JSString* ret = JS_NewUCString(cx, utf16.get(), len);
+
+    if(ret) {
+        // JS_NewUCString took ownership on success. We shift
+        // the resulting pointer into Unused to silence the
+        // compiler warning.
+        mozilla::Unused << utf16.release();
     }
 
-    fprintf(stderr, "Unable to allocate string object.\n");
-    exit(3);
+    return ret;
 }
 
 size_t
@@ -84,21 +107,21 @@ couch_readfile(const char* file, char** outbuf_p)
 
     while((nread = fread(fbuf, 1, 16384, fp)) > 0) {
         if(buf == NULL) {
-            buf = (char*) malloc(nread + 1);
+            buf = new char[nread + 1];
             if(buf == NULL) {
                 fprintf(stderr, "Out of memory.\n");
                 exit(3);
             }
             memcpy(buf, fbuf, nread);
         } else {
-            tmp = (char*) malloc(buflen + nread + 1);
+            tmp = new char[buflen + nread + 1];
             if(tmp == NULL) {
                 fprintf(stderr, "Out of memory.\n");
                 exit(3);
             }
             memcpy(tmp, buf, buflen);
             memcpy(tmp+buflen, fbuf, nread);
-            free(buf);
+            delete buf;
             buf = tmp;
         }
         buflen += nread;
@@ -114,12 +137,17 @@ couch_parse_args(int argc, const char* argv[])
     couch_args* args;
     int i = 1;
 
-    args = (couch_args*) malloc(sizeof(couch_args));
+    args = new couch_args();
     if(args == NULL)
         return NULL;
 
-    memset(args, '\0', sizeof(couch_args));
+    args->eval = 0;
+    args->use_http = 0;
+    args->use_test_funs = 0;
     args->stack_size = 64L * 1024L * 1024L;
+    args->scripts = nullptr;
+    args->uri_file = nullptr;
+    args->uri = nullptr;
 
     while(i < argc) {
         if(strcmp("-h", argv[i]) == 0) {
@@ -193,7 +221,7 @@ couch_readline(JSContext* cx, FILE* fp)
     size_t oldbyteslen = 256;
     size_t readlen = 0;
 
-    bytes = (char *)JS_malloc(cx, byteslen);
+    bytes = static_cast<char*>(JS_malloc(cx, byteslen));
     if(bytes == NULL) return NULL;
     
     while((readlen = couch_fgets(bytes+used, byteslen-used, fp)) > 0) {
@@ -207,7 +235,7 @@ couch_readline(JSContext* cx, FILE* fp)
         // Double our buffer and read more.
         oldbyteslen = byteslen;
         byteslen *= 2;
-        tmp = (char *)JS_realloc(cx, bytes, oldbyteslen, byteslen);
+        tmp = static_cast<char*>(JS_realloc(cx, bytes, oldbyteslen, byteslen));
         if(!tmp) {
             JS_free(cx, bytes);
             return NULL;
@@ -222,8 +250,8 @@ couch_readline(JSContext* cx, FILE* fp)
         return JS_NewStringCopyZ(cx, nullptr);
     }
 
-    // Shring the buffer to the actual data size
-    tmp = (char *)JS_realloc(cx, bytes, byteslen, used);
+    // Shrink the buffer to the actual data size
+    tmp = static_cast<char*>(JS_realloc(cx, bytes, byteslen, used));
     if(!tmp) {
         JS_free(cx, bytes);
         return NULL;
@@ -238,22 +266,16 @@ couch_readline(JSContext* cx, FILE* fp)
 
 
 void
-couch_print(JSContext* cx, unsigned int argc, JS::CallArgs argv)
+couch_print(JSContext* cx, JS::HandleValue obj, bool use_stderr)
 {
-    uint8_t* bytes = nullptr;
-    FILE *stream = stdout;
+    FILE* stream = stdout;
 
-    if (argc) {
-        if (argc > 1 && argv[1].isTrue()) {
-          stream = stderr;
-        }
-        JSString* str = JS::ToString(cx, argv.get(0));
-        bytes = reinterpret_cast<uint8_t*>(JS_EncodeString(cx, str));
-        fprintf(stream, "%s", bytes);
-        JS_free(cx, bytes);
+    if(use_stderr) {
+        stream = stderr;
     }
 
-    fputc('\n', stream);
+    std::string val = js_to_string(cx, obj);
+    fprintf(stream, "%s\n", val.c_str());
     fflush(stream);
 }
 
@@ -261,51 +283,63 @@ couch_print(JSContext* cx, unsigned int argc, JS::CallArgs argv)
 void
 couch_error(JSContext* cx, JSErrorReport* report)
 {
-    JS::RootedValue v(cx), stack(cx), replace(cx);
-    char* bytes;
-    JSObject* regexp;
-
-    if(!report || !JSREPORT_IS_WARNING(report->flags))
-    {
-        fprintf(stderr, "%s\n", report->message().c_str());
-
-        // Print a stack trace, if available.
-        if (JSREPORT_IS_EXCEPTION(report->flags) &&
-            JS_GetPendingException(cx, &v))
-        {
-            // Clear the exception before an JS method calls or the result is
-            // infinite, recursive error report generation.
-            JS_ClearPendingException(cx);
-
-            // Use JS regexp to indent the stack trace.
-            // If the regexp can't be created, don't JS_ReportErrorUTF8 since it is
-            // probably not productive to wind up here again.
-            JS::RootedObject vobj(cx, v.toObjectOrNull());
-
-            if(JS_GetProperty(cx, vobj, "stack", &stack) &&
-               (regexp = JS_NewRegExpObject(
-                   cx, "^(?=.)", 6, JSREG_GLOB | JSREG_MULTILINE)))
-            {
-                // Set up the arguments to ``String.replace()``
-                JS::AutoValueVector re_args(cx);
-                JS::RootedValue arg0(cx, JS::ObjectValue(*regexp));
-                auto arg1 = JS::StringValue(string_to_js(cx, "\t"));
-
-                if (re_args.append(arg0) && re_args.append(arg1)) {
-                    // Perform the replacement
-                    JS::RootedObject sobj(cx, stack.toObjectOrNull());
-                    if(JS_GetProperty(cx, sobj, "replace", &replace) &&
-                       JS_CallFunctionValue(cx, sobj, replace, re_args, &v))
-                    {
-                        // Print the result
-                        bytes = enc_string(cx, v, NULL);
-                        fprintf(stderr, "Stacktrace:\n%s", bytes);
-                        JS_free(cx, bytes);
-                    }
-                }
-            }
+    if(!report) {
+        return;
+    }
+
+    if(JSREPORT_IS_WARNING(report->flags)) {
+        return;
+    }
+
+    std::ostringstream msg;
+    msg << "error: " << report->message().c_str();
+
+    mozilla::Maybe<JSAutoCompartment> ac;
+    JS::RootedValue exc(cx);
+    JS::RootedObject exc_obj(cx);
+    JS::RootedObject stack_obj(cx);
+    JS::RootedString stack_str(cx);
+    JS::RootedValue stack_val(cx);
+
+    if(!JS_GetPendingException(cx, &exc)) {
+        goto done;
+    }
+
+    // Clear the exception before an JS method calls or the result is
+    // infinite, recursive error report generation.
+    JS_ClearPendingException(cx);
+
+    exc_obj.set(exc.toObjectOrNull());
+    stack_obj.set(JS::ExceptionStackOrNull(exc_obj));
+
+    if(!stack_obj) {
+        // Compilation errors don't have a stack
+
+        msg << " at ";
+
+        if(report->filename) {
+            msg << report->filename;
+        } else {
+            msg << "<unknown>";
+        }
+
+        if(report->lineno) {
+            msg << ':' << report->lineno << ':' << report->column;
         }
+
+        goto done;
+    }
+
+    if(!JS::BuildStackString(cx, stack_obj, &stack_str, 2)) {
+        goto done;
     }
+
+    stack_val.set(JS::StringValue(stack_str));
+    msg << std::endl << std::endl << js_to_string(cx, stack_val).c_str();
+
+done:
+    msg << std::endl;
+    fprintf(stderr, "%s", msg.str().c_str());
 }
 
 
diff --git a/src/couch/priv/couch_js/60/util.h b/src/couch/priv/couch_js/60/util.h
index 407e3e6..4c27f0f 100644
--- a/src/couch/priv/couch_js/60/util.h
+++ b/src/couch/priv/couch_js/60/util.h
@@ -26,14 +26,14 @@ typedef struct {
 } couch_args;
 
 std::string js_to_string(JSContext* cx, JS::HandleValue val);
-std::string js_to_string(JSContext* cx, JSString *str);
+bool js_to_string(JSContext* cx, JS::HandleValue val, std::string& str);
 JSString* string_to_js(JSContext* cx, const std::string& s);
 
 couch_args* couch_parse_args(int argc, const char* argv[]);
 int couch_fgets(char* buf, int size, FILE* fp);
 JSString* couch_readline(JSContext* cx, FILE* fp);
 size_t couch_readfile(const char* file, char** outbuf_p);
-void couch_print(JSContext* cx, unsigned int argc, JS::CallArgs argv);
+void couch_print(JSContext* cx, JS::HandleValue str, bool use_stderr);
 void couch_error(JSContext* cx, JSErrorReport* report);
 void couch_oom(JSContext* cx, void* data);
 bool couch_load_funcs(JSContext* cx, JS::HandleObject obj, JSFunctionSpec* funcs);
diff --git a/src/couch/test/eunit/couch_js_tests.erl b/src/couch/test/eunit/couch_js_tests.erl
index cd6452c..c2c6246 100644
--- a/src/couch/test/eunit/couch_js_tests.erl
+++ b/src/couch/test/eunit/couch_js_tests.erl
@@ -14,17 +14,6 @@
 -include_lib("eunit/include/eunit.hrl").
 
 
--define(FUNC, <<
-  "var state = [];\n"
-  "function(doc) {\n"
-  "  var val = \"0123456789ABCDEF\";\n"
-  "  for(var i = 0; i < 165535; i++) {\n"
-  "    state.push([val, val]);\n"
-  "  }\n"
-  "}\n"
->>).
-
-
 couch_js_test_() ->
     {
         "Test couchjs",
@@ -33,15 +22,142 @@ couch_js_test_() ->
             fun test_util:start_couch/0,
             fun test_util:stop_couch/1,
             [
+                fun should_create_sandbox/0,
+                fun should_roundtrip_utf8/0,
+                fun should_roundtrip_modified_utf8/0,
+                fun should_replace_broken_utf16/0,
+                fun should_allow_js_string_mutations/0,
                 {timeout, 60000, fun should_exit_on_oom/0}
             ]
         }
     }.
 
 
+should_create_sandbox() ->
+    % Try and detect whether we can see out of the
+    % sandbox or not.
+    Src = <<
+      "function(doc) {\n"
+      "  try {\n"
+      "    emit(false, typeof(Couch.compile_function));\n"
+      "  } catch (e) {\n"
+      "    emit(true, e.message);\n"
+      "  }\n"
+      "}\n"
+    >>,
+    Proc = couch_query_servers:get_os_process(<<"javascript">>),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+    Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, <<"{}">>]),
+    ?assertEqual([[[true, <<"Couch is not defined">>]]], Result).
+
+
+should_roundtrip_utf8() ->
+    % Try round tripping UTF-8 both directions through
+    % couchjs. These tests use hex encoded values of
+    % Ä (C384) and Ü (C39C) so as to avoid odd editor/Erlang encoding
+    % strangeness.
+    Src = <<
+      "function(doc) {\n"
+      "  emit(doc.value, \"", 16#C3, 16#9C, "\");\n"
+      "}\n"
+    >>,
+    Proc = couch_query_servers:get_os_process(<<"javascript">>),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+    Doc = {[
+        {<<"value">>, <<16#C3, 16#84>>}
+    ]},
+    Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+    ?assertEqual([[[<<16#C3, 16#84>>, <<16#C3, 16#9C>>]]], Result).
+
+
+should_roundtrip_modified_utf8() ->
+    % Mimicing the test case from the mailing list
+    Src = <<
+      "function(doc) {\n"
+      "  emit(doc.value.toLowerCase(), \"", 16#C3, 16#9C, "\");\n"
+      "}\n"
+    >>,
+    Proc = couch_query_servers:get_os_process(<<"javascript">>),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+    Doc = {[
+        {<<"value">>, <<16#C3, 16#84>>}
+    ]},
+    Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+    ?assertEqual([[[<<16#C3, 16#A4>>, <<16#C3, 16#9C>>]]], Result).
+
+
+should_replace_broken_utf16() ->
+    % This test reverse the surrogate pair of
+    % the Boom emoji U+1F4A5
+    Src = <<
+      "function(doc) {\n"
+      "  emit(doc.value.split(\"\").reverse().join(\"\"), 1);\n"
+      "}\n"
+    >>,
+    Proc = couch_query_servers:get_os_process(<<"javascript">>),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+    Doc = {[
+        {<<"value">>, list_to_binary(xmerl_ucs:to_utf8([16#1F4A5]))}
+    ]},
+    Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+    % Invalid UTF-8 gets replaced with the 16#FFFD replacement
+    % marker
+    Markers = list_to_binary(xmerl_ucs:to_utf8([16#FFFD, 16#FFFD])),
+    ?assertEqual([[[Markers, 1]]], Result).
+
+
+should_allow_js_string_mutations() ->
+    % This binary corresponds to this string: мама мыла раму
+    % Which I'm told translates to: "mom was washing the frame"
+    MomWashedTheFrame = <<
+        16#D0, 16#BC, 16#D0, 16#B0, 16#D0, 16#BC, 16#D0, 16#B0, 16#20,
+        16#D0, 16#BC, 16#D1, 16#8B, 16#D0, 16#BB, 16#D0, 16#B0, 16#20,
+        16#D1, 16#80, 16#D0, 16#B0, 16#D0, 16#BC, 16#D1, 16#83
+    >>,
+    Mom = <<16#D0, 16#BC, 16#D0, 16#B0, 16#D0, 16#BC, 16#D0, 16#B0>>,
+    Washed = <<16#D0, 16#BC, 16#D1, 16#8B, 16#D0, 16#BB, 16#D0, 16#B0>>,
+    Src1 = <<
+      "function(doc) {\n"
+      "  emit(\"length\", doc.value.length);\n"
+      "}\n"
+    >>,
+    Src2 = <<
+      "function(doc) {\n"
+      "  emit(\"substring\", doc.value.substring(5, 9));\n"
+      "}\n"
+    >>,
+    Src3 = <<
+      "function(doc) {\n"
+      "  emit(\"slice\", doc.value.slice(0, 4));\n"
+      "}\n"
+    >>,
+    Proc = couch_query_servers:get_os_process(<<"javascript">>),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src1]),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src2]),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src3]),
+    Doc = {[{<<"value">>, MomWashedTheFrame}]},
+    Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+    io:format(standard_error, "~w~n~w~n", [MomWashedTheFrame, Result]),
+    Expect = [
+        [[<<"length">>, 14]],
+        [[<<"substring">>, Washed]],
+        [[<<"slice">>, Mom]]
+    ],
+    ?assertEqual(Expect, Result).
+
+
 should_exit_on_oom() ->
+    Src = <<
+      "var state = [];\n"
+      "function(doc) {\n"
+      "  var val = \"0123456789ABCDEF\";\n"
+      "  for(var i = 0; i < 165535; i++) {\n"
+      "    state.push([val, val]);\n"
+      "  }\n"
+      "}\n"
+    >>,
     Proc = couch_query_servers:get_os_process(<<"javascript">>),
-    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, ?FUNC]),
+    true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
     trigger_oom(Proc).
 
 trigger_oom(Proc) ->