js/src/jsapi-tests/testParseJSON.cpp
author Jon Coppeard <jcoppeard@mozilla.com>
Mon, 14 Jan 2019 11:02:35 +0000
changeset 453704 1cebf4f5c850ad22500a360c2742de3c9dd78c81
parent 448963 66eb1f485c1a3ea81372758bc92292c9428b17cd
permissions -rw-r--r--
Bug 1519397 - Factor GC locking RAII classes out of vm/Runtime.h r=pbone

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <limits>
#include <string.h>

#include "builtin/String.h"

#include "js/JSON.h"
#include "js/MemoryFunctions.h"
#include "js/Printf.h"
#include "jsapi-tests/tests.h"

using namespace js;

class AutoInflatedString {
  JSContext* const cx;
  char16_t* chars_;
  size_t length_;

 public:
  explicit AutoInflatedString(JSContext* cx)
      : cx(cx), chars_(nullptr), length_(0) {}
  ~AutoInflatedString() { JS_free(cx, chars_); }

  template <size_t N>
  void operator=(const char (&str)[N]) {
    length_ = N - 1;
    chars_ = InflateString(cx, str, length_);
    if (!chars_) {
      abort();
    }
  }

  void operator=(const char* str) {
    length_ = strlen(str);
    chars_ = InflateString(cx, str, length_);
    if (!chars_) {
      abort();
    }
  }

  const char16_t* chars() const { return chars_; }
  size_t length() const { return length_; }
};

BEGIN_TEST(testParseJSON_success) {
  // Primitives
  JS::RootedValue expected(cx);
  expected = JS::TrueValue();
  CHECK(TryParse(cx, "true", expected));

  expected = JS::FalseValue();
  CHECK(TryParse(cx, "false", expected));

  expected = JS::NullValue();
  CHECK(TryParse(cx, "null", expected));

  expected.setInt32(0);
  CHECK(TryParse(cx, "0", expected));

  expected.setInt32(1);
  CHECK(TryParse(cx, "1", expected));

  expected.setInt32(-1);
  CHECK(TryParse(cx, "-1", expected));

  expected.setDouble(1);
  CHECK(TryParse(cx, "1", expected));

  expected.setDouble(1.75);
  CHECK(TryParse(cx, "1.75", expected));

  expected.setDouble(9e9);
  CHECK(TryParse(cx, "9e9", expected));

  expected.setDouble(std::numeric_limits<double>::infinity());
  CHECK(TryParse(cx, "9e99999", expected));

  JS::Rooted<JSFlatString*> str(cx);

  const char16_t emptystr[] = {'\0'};
  str = js::NewStringCopyN<CanGC>(cx, emptystr, 0);
  CHECK(str);
  expected = JS::StringValue(str);
  CHECK(TryParse(cx, "\"\"", expected));

  const char16_t nullstr[] = {'\0'};
  str = NewString(cx, nullstr);
  CHECK(str);
  expected = JS::StringValue(str);
  CHECK(TryParse(cx, "\"\\u0000\"", expected));

  const char16_t backstr[] = {'\b'};
  str = NewString(cx, backstr);
  CHECK(str);
  expected = JS::StringValue(str);
  CHECK(TryParse(cx, "\"\\b\"", expected));
  CHECK(TryParse(cx, "\"\\u0008\"", expected));

  const char16_t newlinestr[] = {
      '\n',
  };
  str = NewString(cx, newlinestr);
  CHECK(str);
  expected = JS::StringValue(str);
  CHECK(TryParse(cx, "\"\\n\"", expected));
  CHECK(TryParse(cx, "\"\\u000A\"", expected));

  // Arrays
  JS::RootedValue v(cx), v2(cx);
  JS::RootedObject obj(cx);

  bool isArray;

  CHECK(Parse(cx, "[]", &v));
  CHECK(v.isObject());
  obj = &v.toObject();
  CHECK(JS_IsArrayObject(cx, obj, &isArray));
  CHECK(isArray);
  CHECK(JS_GetProperty(cx, obj, "length", &v2));
  CHECK(v2.isInt32(0));

  CHECK(Parse(cx, "[1]", &v));
  CHECK(v.isObject());
  obj = &v.toObject();
  CHECK(JS_IsArrayObject(cx, obj, &isArray));
  CHECK(isArray);
  CHECK(JS_GetProperty(cx, obj, "0", &v2));
  CHECK(v2.isInt32(1));
  CHECK(JS_GetProperty(cx, obj, "length", &v2));
  CHECK(v2.isInt32(1));

  // Objects
  CHECK(Parse(cx, "{}", &v));
  CHECK(v.isObject());
  obj = &v.toObject();
  CHECK(JS_IsArrayObject(cx, obj, &isArray));
  CHECK(!isArray);

  CHECK(Parse(cx, "{ \"f\": 17 }", &v));
  CHECK(v.isObject());
  obj = &v.toObject();
  CHECK(JS_IsArrayObject(cx, obj, &isArray));
  CHECK(!isArray);
  CHECK(JS_GetProperty(cx, obj, "f", &v2));
  CHECK(v2.isInt32(17));

  return true;
}

template <size_t N>
static JSFlatString* NewString(JSContext* cx, const char16_t (&chars)[N]) {
  return js::NewStringCopyN<CanGC>(cx, chars, N);
}

template <size_t N>
inline bool Parse(JSContext* cx, const char (&input)[N],
                  JS::MutableHandleValue vp) {
  AutoInflatedString str(cx);
  str = input;
  CHECK(JS_ParseJSON(cx, str.chars(), str.length(), vp));
  return true;
}

template <size_t N>
inline bool TryParse(JSContext* cx, const char (&input)[N],
                     JS::HandleValue expected) {
  AutoInflatedString str(cx);
  RootedValue v(cx);
  str = input;
  CHECK(JS_ParseJSON(cx, str.chars(), str.length(), &v));
  CHECK_SAME(v, expected);
  return true;
}
END_TEST(testParseJSON_success)

BEGIN_TEST(testParseJSON_error) {
  CHECK(Error(cx, "", 1, 1));
  CHECK(Error(cx, "\n", 2, 1));
  CHECK(Error(cx, "\r", 2, 1));
  CHECK(Error(cx, "\r\n", 2, 1));

  CHECK(Error(cx, "[", 1, 2));
  CHECK(Error(cx, "[,]", 1, 2));
  CHECK(Error(cx, "[1,]", 1, 4));
  CHECK(Error(cx, "{a:2}", 1, 2));
  CHECK(Error(cx, "{\"a\":2,}", 1, 8));
  CHECK(Error(cx, "]", 1, 1));
  CHECK(Error(cx, "\"", 1, 2));
  CHECK(Error(cx, "{]", 1, 2));
  CHECK(Error(cx, "[}", 1, 2));
  CHECK(Error(cx, "'wrongly-quoted string'", 1, 1));

  CHECK(Error(cx, "{\"a\":2 \n b:3}", 2, 2));
  CHECK(Error(cx, "\n[", 2, 2));
  CHECK(Error(cx, "\n[,]", 2, 2));
  CHECK(Error(cx, "\n[1,]", 2, 4));
  CHECK(Error(cx, "\n{a:2}", 2, 2));
  CHECK(Error(cx, "\n{\"a\":2,}", 2, 8));
  CHECK(Error(cx, "\n]", 2, 1));
  CHECK(Error(cx, "\"bad string\n\"", 1, 12));
  CHECK(Error(cx, "\r'wrongly-quoted string'", 2, 1));
  CHECK(Error(cx, "\n\"", 2, 2));
  CHECK(Error(cx, "\n{]", 2, 2));
  CHECK(Error(cx, "\n[}", 2, 2));
  CHECK(Error(cx, "{\"a\":[2,3],\n\"b\":,5,6}", 2, 5));

  CHECK(Error(cx, "{\"a\":2 \r b:3}", 2, 2));
  CHECK(Error(cx, "\r[", 2, 2));
  CHECK(Error(cx, "\r[,]", 2, 2));
  CHECK(Error(cx, "\r[1,]", 2, 4));
  CHECK(Error(cx, "\r{a:2}", 2, 2));
  CHECK(Error(cx, "\r{\"a\":2,}", 2, 8));
  CHECK(Error(cx, "\r]", 2, 1));
  CHECK(Error(cx, "\"bad string\r\"", 1, 12));
  CHECK(Error(cx, "\r'wrongly-quoted string'", 2, 1));
  CHECK(Error(cx, "\r\"", 2, 2));
  CHECK(Error(cx, "\r{]", 2, 2));
  CHECK(Error(cx, "\r[}", 2, 2));
  CHECK(Error(cx, "{\"a\":[2,3],\r\"b\":,5,6}", 2, 5));

  CHECK(Error(cx, "{\"a\":2 \r\n b:3}", 2, 2));
  CHECK(Error(cx, "\r\n[", 2, 2));
  CHECK(Error(cx, "\r\n[,]", 2, 2));
  CHECK(Error(cx, "\r\n[1,]", 2, 4));
  CHECK(Error(cx, "\r\n{a:2}", 2, 2));
  CHECK(Error(cx, "\r\n{\"a\":2,}", 2, 8));
  CHECK(Error(cx, "\r\n]", 2, 1));
  CHECK(Error(cx, "\"bad string\r\n\"", 1, 12));
  CHECK(Error(cx, "\r\n'wrongly-quoted string'", 2, 1));
  CHECK(Error(cx, "\r\n\"", 2, 2));
  CHECK(Error(cx, "\r\n{]", 2, 2));
  CHECK(Error(cx, "\r\n[}", 2, 2));
  CHECK(Error(cx, "{\"a\":[2,3],\r\n\"b\":,5,6}", 2, 5));

  CHECK(Error(cx, "\n\"bad string\n\"", 2, 12));
  CHECK(Error(cx, "\r\"bad string\r\"", 2, 12));
  CHECK(Error(cx, "\r\n\"bad string\r\n\"", 2, 12));

  CHECK(Error(cx, "{\n\"a\":[2,3],\r\"b\":,5,6}", 3, 5));
  CHECK(Error(cx, "{\r\"a\":[2,3],\n\"b\":,5,6}", 3, 5));
  CHECK(Error(cx, "[\"\\t\\q", 1, 6));
  CHECK(Error(cx, "[\"\\t\x00", 1, 5));
  CHECK(Error(cx, "[\"\\t\x01", 1, 5));
  CHECK(Error(cx, "[\"\\t\\\x00", 1, 6));
  CHECK(Error(cx, "[\"\\t\\\x01", 1, 6));

  // Unicode escape errors are messy.  The first bad character could be
  // non-hexadecimal, or it could be absent entirely.  Include tests where
  // there's a bad character, followed by zero to as many characters as are
  // needed to form a complete Unicode escape sequence, plus one.  (The extra
  // characters beyond are valuable because our implementation checks for
  // too-few subsequent characters first, before checking for subsequent
  // non-hexadecimal characters.  So \u<END>, \u0<END>, \u00<END>, and
  // \u000<END> are all *detected* as invalid by the same code path, but the
  // process of computing the first invalid character follows a different
  // code path for each.  And \uQQQQ, \u0QQQ, \u00QQ, and \u000Q are detected
  // as invalid by the same code path [ignoring which precise subexpression
  // triggers failure of a single condition], but the computation of the
  // first invalid character follows a different code path for each.)
  CHECK(Error(cx, "[\"\\t\\u", 1, 7));
  CHECK(Error(cx, "[\"\\t\\uZ", 1, 7));
  CHECK(Error(cx, "[\"\\t\\uZZ", 1, 7));
  CHECK(Error(cx, "[\"\\t\\uZZZ", 1, 7));
  CHECK(Error(cx, "[\"\\t\\uZZZZ", 1, 7));
  CHECK(Error(cx, "[\"\\t\\uZZZZZ", 1, 7));

  CHECK(Error(cx, "[\"\\t\\u0", 1, 8));
  CHECK(Error(cx, "[\"\\t\\u0Z", 1, 8));
  CHECK(Error(cx, "[\"\\t\\u0ZZ", 1, 8));
  CHECK(Error(cx, "[\"\\t\\u0ZZZ", 1, 8));
  CHECK(Error(cx, "[\"\\t\\u0ZZZZ", 1, 8));

  CHECK(Error(cx, "[\"\\t\\u00", 1, 9));
  CHECK(Error(cx, "[\"\\t\\u00Z", 1, 9));
  CHECK(Error(cx, "[\"\\t\\u00ZZ", 1, 9));
  CHECK(Error(cx, "[\"\\t\\u00ZZZ", 1, 9));

  CHECK(Error(cx, "[\"\\t\\u000", 1, 10));
  CHECK(Error(cx, "[\"\\t\\u000Z", 1, 10));
  CHECK(Error(cx, "[\"\\t\\u000ZZ", 1, 10));

  return true;
}

template <size_t N>
inline bool Error(JSContext* cx, const char (&input)[N], uint32_t expectedLine,
                  uint32_t expectedColumn) {
  AutoInflatedString str(cx);
  RootedValue dummy(cx);
  str = input;

  bool ok = JS_ParseJSON(cx, str.chars(), str.length(), &dummy);
  CHECK(!ok);

  RootedValue exn(cx);
  CHECK(JS_GetPendingException(cx, &exn));
  JS_ClearPendingException(cx);

  js::ErrorReport report(cx);
  CHECK(report.init(cx, exn, js::ErrorReport::WithSideEffects));
  CHECK(report.report()->errorNumber == JSMSG_JSON_BAD_PARSE);

  UniqueChars lineAndColumnASCII =
      JS_smprintf("line %d column %d", expectedLine, expectedColumn);
  CHECK(strstr(report.toStringResult().c_str(), lineAndColumnASCII.get()) !=
        nullptr);

  /* We do not execute JS, so there should be no exception thrown. */
  CHECK(!JS_IsExceptionPending(cx));

  return true;
}
END_TEST(testParseJSON_error)

static bool Censor(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
  MOZ_RELEASE_ASSERT(args.length() == 2);
  MOZ_RELEASE_ASSERT(args[0].isString());
  args.rval().setNull();
  return true;
}

BEGIN_TEST(testParseJSON_reviver) {
  JSFunction* fun = JS_NewFunction(cx, Censor, 0, 0, "censor");
  CHECK(fun);

  JS::RootedValue filter(cx, JS::ObjectValue(*JS_GetFunctionObject(fun)));

  CHECK(TryParse(cx, "true", filter));
  CHECK(TryParse(cx, "false", filter));
  CHECK(TryParse(cx, "null", filter));
  CHECK(TryParse(cx, "1", filter));
  CHECK(TryParse(cx, "1.75", filter));
  CHECK(TryParse(cx, "[]", filter));
  CHECK(TryParse(cx, "[1]", filter));
  CHECK(TryParse(cx, "{}", filter));
  return true;
}

template <size_t N>
inline bool TryParse(JSContext* cx, const char (&input)[N],
                     JS::HandleValue filter) {
  AutoInflatedString str(cx);
  JS::RootedValue v(cx);
  str = input;
  CHECK(JS_ParseJSONWithReviver(cx, str.chars(), str.length(), filter, &v));
  CHECK(v.isNull());
  return true;
}
END_TEST(testParseJSON_reviver)