| /* -*- 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; |
| } |