blob: 2aa2ad678874b989398ff04046d60ba9cd8b41a6 [file] [log] [blame]
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* vim: set ts=8 sts=4 et sw=4 tw=99:
* 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 "jsonparser.h"
#include "mozilla/RangedPtr.h"
#include "jsarray.h"
#include "jscompartment.h"
#include "jsnum.h"
#include "vm/StringBuffer.h"
#include "jsobjinlines.h"
using namespace js;
using mozilla::RangedPtr;
JSONParser::~JSONParser()
{
for (size_t i = 0; i < stack.length(); i++) {
if (stack[i].state == FinishArrayElement)
js_delete(&stack[i].elements());
else
js_delete(&stack[i].properties());
}
for (size_t i = 0; i < freeElements.length(); i++)
js_delete(freeElements[i]);
for (size_t i = 0; i < freeProperties.length(); i++)
js_delete(freeProperties[i]);
}
void
JSONParser::trace(JSTracer *trc)
{
for (size_t i = 0; i < stack.length(); i++) {
if (stack[i].state == FinishArrayElement) {
ElementVector &elements = stack[i].elements();
for (size_t j = 0; j < elements.length(); j++)
gc::MarkValueRoot(trc, &elements[j], "JSONParser element");
} else {
PropertyVector &properties = stack[i].properties();
for (size_t j = 0; j < properties.length(); j++) {
gc::MarkValueRoot(trc, &properties[j].value, "JSONParser property value");
gc::MarkIdRoot(trc, &properties[j].id, "JSONParser property id");
}
}
}
}
void
JSONParser::error(const char *msg)
{
if (errorHandling == RaiseError)
JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_JSON_BAD_PARSE, msg);
}
bool
JSONParser::errorReturn()
{
return errorHandling == NoError;
}
template<JSONParser::StringType ST>
JSONParser::Token
JSONParser::readString()
{
JS_ASSERT(current < end);
JS_ASSERT(*current == '"');
/*
* JSONString:
* /^"([^\u0000-\u001F"\\]|\\(["/\\bfnrt]|u[0-9a-fA-F]{4}))*"$/
*/
if (++current == end) {
error("unterminated string literal");
return token(Error);
}
/*
* Optimization: if the source contains no escaped characters, create the
* string directly from the source text.
*/
RangedPtr<const jschar> start = current;
for (; current < end; current++) {
if (*current == '"') {
size_t length = current - start;
current++;
JSFlatString *str = (ST == JSONParser::PropertyName)
? AtomizeChars<CanGC>(cx, start.get(), length)
: js_NewStringCopyN<CanGC>(cx, start.get(), length);
if (!str)
return token(OOM);
return stringToken(str);
}
if (*current == '\\')
break;
if (*current <= 0x001F) {
error("bad control character in string literal");
return token(Error);
}
}
/*
* Slow case: string contains escaped characters. Copy a maximal sequence
* of unescaped characters into a temporary buffer, then an escaped
* character, and repeat until the entire string is consumed.
*/
StringBuffer buffer(cx);
do {
if (start < current && !buffer.append(start.get(), current.get()))
return token(OOM);
if (current >= end)
break;
jschar c = *current++;
if (c == '"') {
JSFlatString *str = (ST == JSONParser::PropertyName)
? buffer.finishAtom()
: buffer.finishString();
if (!str)
return token(OOM);
return stringToken(str);
}
if (c != '\\') {
error("bad character in string literal");
return token(Error);
}
if (current >= end)
break;
switch (*current++) {
case '"': c = '"'; break;
case '/': c = '/'; break;
case '\\': c = '\\'; break;
case 'b': c = '\b'; break;
case 'f': c = '\f'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'u':
if (end - current < 4) {
error("bad Unicode escape");
return token(Error);
}
if (JS7_ISHEX(current[0]) &&
JS7_ISHEX(current[1]) &&
JS7_ISHEX(current[2]) &&
JS7_ISHEX(current[3]))
{
c = (JS7_UNHEX(current[0]) << 12)
| (JS7_UNHEX(current[1]) << 8)
| (JS7_UNHEX(current[2]) << 4)
| (JS7_UNHEX(current[3]));
current += 4;
break;
}
/* FALL THROUGH */
default:
error("bad escaped character");
return token(Error);
}
if (!buffer.append(c))
return token(OOM);
start = current;
for (; current < end; current++) {
if (*current == '"' || *current == '\\' || *current <= 0x001F)
break;
}
} while (current < end);
error("unterminated string");
return token(Error);
}
JSONParser::Token
JSONParser::readNumber()
{
JS_ASSERT(current < end);
JS_ASSERT(JS7_ISDEC(*current) || *current == '-');
/*
* JSONNumber:
* /^-?(0|[1-9][0-9]+)(\.[0-9]+)?([eE][\+\-]?[0-9]+)?$/
*/
bool negative = *current == '-';
/* -? */
if (negative && ++current == end) {
error("no number after minus sign");
return token(Error);
}
const RangedPtr<const jschar> digitStart = current;
/* 0|[1-9][0-9]+ */
if (!JS7_ISDEC(*current)) {
error("unexpected non-digit");
return token(Error);
}
if (*current++ != '0') {
for (; current < end; current++) {
if (!JS7_ISDEC(*current))
break;
}
}
/* Fast path: no fractional or exponent part. */
if (current == end || (*current != '.' && *current != 'e' && *current != 'E')) {
TwoByteChars chars(digitStart.get(), current - digitStart);
if (chars.length() < strlen("9007199254740992")) {
// If the decimal number is shorter than the length of 2**53, (the
// largest number a double can represent with integral precision),
// parse it using a decimal-only parser. This comparison is
// conservative but faster than a fully-precise check.
double d = ParseDecimalNumber(chars);
return numberToken(negative ? -d : d);
}
double d;
const jschar *dummy;
if (!GetPrefixInteger(cx, digitStart.get(), current.get(), 10, &dummy, &d))
return token(OOM);
JS_ASSERT(current == dummy);
return numberToken(negative ? -d : d);
}
/* (\.[0-9]+)? */
if (current < end && *current == '.') {
if (++current == end) {
error("missing digits after decimal point");
return token(Error);
}
if (!JS7_ISDEC(*current)) {
error("unterminated fractional number");
return token(Error);
}
while (++current < end) {
if (!JS7_ISDEC(*current))
break;
}
}
/* ([eE][\+\-]?[0-9]+)? */
if (current < end && (*current == 'e' || *current == 'E')) {
if (++current == end) {
error("missing digits after exponent indicator");
return token(Error);
}
if (*current == '+' || *current == '-') {
if (++current == end) {
error("missing digits after exponent sign");
return token(Error);
}
}
if (!JS7_ISDEC(*current)) {
error("exponent part is missing a number");
return token(Error);
}
while (++current < end) {
if (!JS7_ISDEC(*current))
break;
}
}
double d;
const jschar *finish;
if (!js_strtod(cx, digitStart.get(), current.get(), &finish, &d))
return token(OOM);
JS_ASSERT(current == finish);
return numberToken(negative ? -d : d);
}
static inline bool
IsJSONWhitespace(jschar c)
{
return c == '\t' || c == '\r' || c == '\n' || c == ' ';
}
JSONParser::Token
JSONParser::advance()
{
while (current < end && IsJSONWhitespace(*current))
current++;
if (current >= end) {
error("unexpected end of data");
return token(Error);
}
switch (*current) {
case '"':
return readString<LiteralValue>();
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return readNumber();
case 't':
if (end - current < 4 || current[1] != 'r' || current[2] != 'u' || current[3] != 'e') {
error("unexpected keyword");
return token(Error);
}
current += 4;
return token(True);
case 'f':
if (end - current < 5 ||
current[1] != 'a' || current[2] != 'l' || current[3] != 's' || current[4] != 'e')
{
error("unexpected keyword");
return token(Error);
}
current += 5;
return token(False);
case 'n':
if (end - current < 4 || current[1] != 'u' || current[2] != 'l' || current[3] != 'l') {
error("unexpected keyword");
return token(Error);
}
current += 4;
return token(Null);
case '[':
current++;
return token(ArrayOpen);
case ']':
current++;
return token(ArrayClose);
case '{':
current++;
return token(ObjectOpen);
case '}':
current++;
return token(ObjectClose);
case ',':
current++;
return token(Comma);
case ':':
current++;
return token(Colon);
default:
error("unexpected character");
return token(Error);
}
}
JSONParser::Token
JSONParser::advanceAfterObjectOpen()
{
JS_ASSERT(current[-1] == '{');
while (current < end && IsJSONWhitespace(*current))
current++;
if (current >= end) {
error("end of data while reading object contents");
return token(Error);
}
if (*current == '"')
return readString<PropertyName>();
if (*current == '}') {
current++;
return token(ObjectClose);
}
error("expected property name or '}'");
return token(Error);
}
static inline void
AssertPastValue(const RangedPtr<const jschar> current)
{
/*
* We're past an arbitrary JSON value, so the previous character is
* *somewhat* constrained, even if this assertion is pretty broad. Don't
* knock it till you tried it: this assertion *did* catch a bug once.
*/
JS_ASSERT((current[-1] == 'l' &&
current[-2] == 'l' &&
current[-3] == 'u' &&
current[-4] == 'n') ||
(current[-1] == 'e' &&
current[-2] == 'u' &&
current[-3] == 'r' &&
current[-4] == 't') ||
(current[-1] == 'e' &&
current[-2] == 's' &&
current[-3] == 'l' &&
current[-4] == 'a' &&
current[-5] == 'f') ||
current[-1] == '}' ||
current[-1] == ']' ||
current[-1] == '"' ||
JS7_ISDEC(current[-1]));
}
JSONParser::Token
JSONParser::advanceAfterArrayElement()
{
AssertPastValue(current);
while (current < end && IsJSONWhitespace(*current))
current++;
if (current >= end) {
error("end of data when ',' or ']' was expected");
return token(Error);
}
if (*current == ',') {
current++;
return token(Comma);
}
if (*current == ']') {
current++;
return token(ArrayClose);
}
error("expected ',' or ']' after array element");
return token(Error);
}
JSONParser::Token
JSONParser::advancePropertyName()
{
JS_ASSERT(current[-1] == ',');
while (current < end && IsJSONWhitespace(*current))
current++;
if (current >= end) {
error("end of data when property name was expected");
return token(Error);
}
if (*current == '"')
return readString<PropertyName>();
error("expected double-quoted property name");
return token(Error);
}
JSONParser::Token
JSONParser::advancePropertyColon()
{
JS_ASSERT(current[-1] == '"');
while (current < end && IsJSONWhitespace(*current))
current++;
if (current >= end) {
error("end of data after property name when ':' was expected");
return token(Error);
}
if (*current == ':') {
current++;
return token(Colon);
}
error("expected ':' after property name in object");
return token(Error);
}
JSONParser::Token
JSONParser::advanceAfterProperty()
{
AssertPastValue(current);
while (current < end && IsJSONWhitespace(*current))
current++;
if (current >= end) {
error("end of data after property value in object");
return token(Error);
}
if (*current == ',') {
current++;
return token(Comma);
}
if (*current == '}') {
current++;
return token(ObjectClose);
}
error("expected ',' or '}' after property value in object");
return token(Error);
}
JSObject *
JSONParser::createFinishedObject(PropertyVector &properties)
{
/*
* Look for an existing cached type and shape for objects with this set of
* properties.
*/
if (cx->typeInferenceEnabled()) {
JSObject *obj = cx->compartment()->types.newTypedObject(cx, properties.begin(),
properties.length());
if (obj)
return obj;
}
/*
* Make a new object sized for the given number of properties and fill its
* shape in manually.
*/
gc::AllocKind allocKind = gc::GetGCObjectKind(properties.length());
RootedObject obj(cx, NewBuiltinClassInstance(cx, &ObjectClass, allocKind));
if (!obj)
return NULL;
RootedId propid(cx);
RootedValue value(cx);
for (size_t i = 0; i < properties.length(); i++) {
propid = properties[i].id;
value = properties[i].value;
if (!DefineNativeProperty(cx, obj, propid, value,
JS_PropertyStub, JS_StrictPropertyStub, JSPROP_ENUMERATE,
0, 0))
{
return NULL;
}
}
/*
* Try to assign a new type to the object with type information for its
* properties, and update the initializer type object cache with this
* object's final shape.
*/
if (cx->typeInferenceEnabled())
cx->compartment()->types.fixObjectType(cx, obj);
return obj;
}
inline bool
JSONParser::finishObject(MutableHandleValue vp, PropertyVector &properties)
{
JS_ASSERT(&properties == &stack.back().properties());
JSObject *obj = createFinishedObject(properties);
if (!obj)
return false;
vp.setObject(*obj);
if (!freeProperties.append(&properties))
return false;
stack.popBack();
return true;
}
inline bool
JSONParser::finishArray(MutableHandleValue vp, ElementVector &elements)
{
JS_ASSERT(&elements == &stack.back().elements());
JSObject *obj = NewDenseCopiedArray(cx, elements.length(), elements.begin());
if (!obj)
return false;
/* Try to assign a new type to the array according to its elements. */
if (cx->typeInferenceEnabled())
cx->compartment()->types.fixArrayType(cx, obj);
vp.setObject(*obj);
if (!freeElements.append(&elements))
return false;
stack.popBack();
return true;
}
bool
JSONParser::parse(MutableHandleValue vp)
{
RootedValue value(cx);
JS_ASSERT(stack.empty());
vp.setUndefined();
Token token;
ParserState state = JSONValue;
while (true) {
switch (state) {
case FinishObjectMember: {
PropertyVector &properties = stack.back().properties();
properties.back().value = value;
token = advanceAfterProperty();
if (token == ObjectClose) {
if (!finishObject(&value, properties))
return false;
break;
}
if (token != Comma) {
if (token == OOM)
return false;
if (token != Error)
error("expected ',' or '}' after property-value pair in object literal");
return errorReturn();
}
token = advancePropertyName();
/* FALL THROUGH */
}
JSONMember:
if (token == String) {
jsid id = AtomToId(atomValue());
PropertyVector &properties = stack.back().properties();
if (!properties.append(IdValuePair(id)))
return false;
token = advancePropertyColon();
if (token != Colon) {
JS_ASSERT(token == Error);
return errorReturn();
}
goto JSONValue;
}
if (token == OOM)
return false;
if (token != Error)
error("property names must be double-quoted strings");
return errorReturn();
case FinishArrayElement: {
ElementVector &elements = stack.back().elements();
if (!elements.append(value.get()))
return false;
token = advanceAfterArrayElement();
if (token == Comma)
goto JSONValue;
if (token == ArrayClose) {
if (!finishArray(&value, elements))
return false;
break;
}
JS_ASSERT(token == Error);
return errorReturn();
}
JSONValue:
case JSONValue:
token = advance();
JSONValueSwitch:
switch (token) {
case String:
value = stringValue();
break;
case Number:
value = numberValue();
break;
case True:
value = BooleanValue(true);
break;
case False:
value = BooleanValue(false);
break;
case Null:
value = NullValue();
break;
case ArrayOpen: {
ElementVector *elements;
if (!freeElements.empty()) {
elements = freeElements.popCopy();
elements->clear();
} else {
elements = cx->new_<ElementVector>(cx);
if (!elements)
return false;
}
if (!stack.append(elements))
return false;
token = advance();
if (token == ArrayClose) {
if (!finishArray(&value, *elements))
return false;
break;
}
goto JSONValueSwitch;
}
case ObjectOpen: {
PropertyVector *properties;
if (!freeProperties.empty()) {
properties = freeProperties.popCopy();
properties->clear();
} else {
properties = cx->new_<PropertyVector>(cx);
if (!properties)
return false;
}
if (!stack.append(properties))
return false;
token = advanceAfterObjectOpen();
if (token == ObjectClose) {
if (!finishObject(&value, *properties))
return false;
break;
}
goto JSONMember;
}
case ArrayClose:
case ObjectClose:
case Colon:
case Comma:
error("unexpected character");
return errorReturn();
case OOM:
return false;
case Error:
return errorReturn();
}
break;
}
if (stack.empty())
break;
state = stack.back().state;
}
for (; current < end; current++) {
if (!IsJSONWhitespace(*current)) {
error("unexpected non-whitespace character after JSON data");
return errorReturn();
}
}
JS_ASSERT(end == current);
JS_ASSERT(stack.empty());
vp.set(value);
return true;
}