|
//------------------------------------------------------------------------------
// <copyright file="JavaScriptObjectDeserializer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Web.Script.Serialization {
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Resources;
using AppSettings = System.Web.Util.AppSettings;
using Debug = System.Web.Util.Debug;
using Utf16StringValidator = System.Web.Util.Utf16StringValidator;
internal class JavaScriptObjectDeserializer {
private const string DateTimePrefix = @"""\/Date(";
private const int DateTimePrefixLength = 8;
private const string DateTimeSuffix = @"\/""";
private const int DateTimeSuffixLength = 3;
internal JavaScriptString _s;
private JavaScriptSerializer _serializer;
private int _depthLimit;
internal static object BasicDeserialize(string input, int depthLimit, JavaScriptSerializer serializer) {
JavaScriptObjectDeserializer jsod = new JavaScriptObjectDeserializer(input, depthLimit, serializer);
object result = jsod.DeserializeInternal(0);
if (jsod._s.GetNextNonEmptyChar() != null) {
throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, AtlasWeb.JSON_IllegalPrimitive, jsod._s.ToString()));
}
return result;
}
private JavaScriptObjectDeserializer(string input, int depthLimit, JavaScriptSerializer serializer) {
_s = new JavaScriptString(input);
_depthLimit = depthLimit;
_serializer = serializer;
}
private object DeserializeInternal(int depth) {
if (++depth > _depthLimit) {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_DepthLimitExceeded));
}
Nullable<Char> c = _s.GetNextNonEmptyChar();
if (c == null) {
return null;
}
_s.MovePrev();
if (IsNextElementDateTime()) {
return DeserializeStringIntoDateTime();
}
if (IsNextElementObject(c)) {
IDictionary<string, object> dict = DeserializeDictionary(depth);
// Try to coerce objects to the right type if they have the __serverType
if (dict.ContainsKey(JavaScriptSerializer.ServerTypeFieldName)) {
return ObjectConverter.ConvertObjectToType(dict, null, _serializer);
}
return dict;
}
if (IsNextElementArray(c)) {
return DeserializeList(depth);
}
if (IsNextElementString(c)) {
return DeserializeString();
}
return DeserializePrimitiveObject();
}
private IList DeserializeList(int depth) {
IList list = new ArrayList();
Nullable<Char> c = _s.MoveNext();
if (c != '[') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidArrayStart));
}
bool expectMore = false;
while ((c = _s.GetNextNonEmptyChar()) != null && c != ']') {
_s.MovePrev();
object o = DeserializeInternal(depth);
list.Add(o);
expectMore = false;
// we might be done here.
c = _s.GetNextNonEmptyChar();
if (c == ']') {
break;
}
expectMore = true;
if (c != ',') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidArrayExpectComma));
}
}
if (expectMore) {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidArrayExtraComma));
}
if (c != ']') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidArrayEnd));
}
return list;
}
private IDictionary<string, object> DeserializeDictionary(int depth) {
IDictionary<string, object> dictionary = null;
Nullable<Char> c = _s.MoveNext();
if (c != '{') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_ExpectedOpenBrace));
}
// Loop through each JSON entry in the input object
while ((c = _s.GetNextNonEmptyChar()) != null) {
_s.MovePrev();
if (c == ':') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidMemberName));
}
string memberName = null;
if (c != '}') {
// Find the member name
memberName = DeserializeMemberName();
c = _s.GetNextNonEmptyChar();
if (c != ':') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidObject));
}
}
if (dictionary == null) {
dictionary = new Dictionary<string, object>();
// If the object contains nothing (i.e. {}), we're done
if (memberName == null) {
// Move the cursor to the '}' character.
c = _s.GetNextNonEmptyChar();
Debug.Assert(c == '}');
break;
}
}
ThrowIfMaxJsonDeserializerMembersExceeded(dictionary.Count);
// Deserialize the property value. Here, we don't know its type
object propVal = DeserializeInternal(depth);
dictionary[memberName] = propVal;
c = _s.GetNextNonEmptyChar();
if (c == '}') {
break;
}
if (c != ',') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidObject));
}
}
if (c != '}') {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_InvalidObject));
}
return dictionary;
}
// MSRC 12038: limit the maximum number of entries that can be added to a Json deserialized dictionary,
// as a large number of entries potentially can result in too many hash collisions that may cause DoS
private void ThrowIfMaxJsonDeserializerMembersExceeded(int count) {
if (count >= AppSettings.MaxJsonDeserializerMembers) {
throw new InvalidOperationException(SR.GetString(SR.CollectionCountExceeded_JavaScriptObjectDeserializer, AppSettings.MaxJsonDeserializerMembers));
}
}
// Deserialize a member name.
// e.g. { MemberName: ... }
// e.g. { 'MemberName': ... }
// e.g. { "MemberName": ... }
private string DeserializeMemberName() {
// It could be double quoted, single quoted, or not quoted at all
Nullable<Char> c = _s.GetNextNonEmptyChar();
if (c == null) {
return null;
}
_s.MovePrev();
// If it's quoted, treat it as a string
if (IsNextElementString(c)) {
return DeserializeString();
}
// Non-quoted token
return DeserializePrimitiveToken();
}
private object DeserializePrimitiveObject() {
string input = DeserializePrimitiveToken();
if (input.Equals("null")) {
return null;
}
if (input.Equals("true")) {
return true;
}
if (input.Equals("false")) {
return false;
}
// Is it a floating point value
bool hasDecimalPoint = input.IndexOf('.') >= 0;
// DevDiv 56892: don't try to parse to Int32/64/Decimal if it has an exponent sign
bool hasExponent = input.LastIndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0;
// [Last]IndexOf(char, StringComparison) overload doesn't exist, so search for "e" as a string not a char
// Use 'Last'IndexOf since if there is an exponent it would be more quickly found starting from the end of the string
// since 'e' is always toward the end of the number. e.g. 1.238907598768972987E82
if (!hasExponent) {
// when no exponent, could be Int32, Int64, Decimal, and may fall back to Double
// otherwise it must be Double
if (!hasDecimalPoint) {
// No decimal or exponent. All Int32 and Int64s fall into this category, so try them first
// First try int
int n;
if (Int32.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out n)) {
// NumberStyles.Integer: AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign
return n;
}
// Then try a long
long l;
if (Int64.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out l)) {
// NumberStyles.Integer: AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign
return l;
}
}
// No exponent, may or may not have a decimal (if it doesn't it couldn't be parsed into Int32/64)
decimal dec;
if (decimal.TryParse(input, NumberStyles.Number, CultureInfo.InvariantCulture, out dec)) {
// NumberStyles.Number: AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign,
// AllowTrailingSign, AllowDecimalPoint, AllowThousands
return dec;
}
}
// either we have an exponent or the number couldn't be parsed into any previous type.
Double d;
if (Double.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out d)) {
// NumberStyles.Float: AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowDecimalPoint, AllowExponent
return d;
}
// must be an illegal primitive
throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, AtlasWeb.JSON_IllegalPrimitive, input));
}
private string DeserializePrimitiveToken() {
StringBuilder sb = new StringBuilder();
Nullable<Char> c = null;
while ((c = _s.MoveNext()) != null) {
if (Char.IsLetterOrDigit(c.Value) || c.Value == '.' ||
c.Value == '-' || c.Value == '_' || c.Value == '+') {
sb.Append(c.Value);
}
else {
_s.MovePrev();
break;
}
}
return sb.ToString();
}
private string DeserializeString() {
StringBuilder sb = new StringBuilder();
bool escapedChar = false;
Nullable<Char> c = _s.MoveNext();
// First determine which quote is used by the string.
Char quoteChar = CheckQuoteChar(c);
while ((c = _s.MoveNext()) != null) {
if (c == '\\') {
if (escapedChar) {
sb.Append('\\');
escapedChar = false;
}
else {
escapedChar = true;
}
continue;
}
if (escapedChar) {
AppendCharToBuilder(c, sb);
escapedChar = false;
}
else {
if (c == quoteChar) {
return Utf16StringValidator.ValidateString(sb.ToString());
}
sb.Append(c.Value);
}
}
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_UnterminatedString));
}
private void AppendCharToBuilder(char? c, StringBuilder sb) {
if (c == '"' || c == '\'' || c == '/') {
sb.Append(c.Value);
}
else if (c == 'b') {
sb.Append('\b');
}
else if (c == 'f') {
sb.Append('\f');
}
else if (c == 'n') {
sb.Append('\n');
}
else if (c == 'r') {
sb.Append('\r');
}
else if (c == 't') {
sb.Append('\t');
}
else if (c == 'u') {
sb.Append((char)int.Parse(_s.MoveNext(4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
}
else {
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_BadEscape));
}
}
private char CheckQuoteChar(char? c) {
Char quoteChar = '"';
if (c == '\'') {
quoteChar = c.Value;
}
else if (c != '"') {
// Fail if the string is not quoted.
throw new ArgumentException(_s.GetDebugString(AtlasWeb.JSON_StringNotQuoted));
}
return quoteChar;
}
private object DeserializeStringIntoDateTime() {
// DivDiv 41127: Never confuse atlas serialized strings with dates.
// DevDiv 74430: JavasciptSerializer will need to handle date time offset - following WCF design
// serialized dates look like: "\/Date(123)\/" or "\/Date(123A)" or "Date(123+4567)" or Date(123-4567)"
// the A, +14567, -4567 portion in the above example is ignored
int pos = _s.IndexOf(DateTimeSuffix);
Match match = Regex.Match(_s.Substring(pos + DateTimeSuffixLength),
@"^""\\/Date\((?<ticks>-?[0-9]+)(?:[a-zA-Z]|(?:\+|-)[0-9]{4})?\)\\/""");
string ticksStr = match.Groups["ticks"].Value;
long ticks;
if (long.TryParse(ticksStr, out ticks)) {
_s.MoveNext(match.Length);
// The javascript ticks start from 1/1/1970 but FX DateTime ticks start from 1/1/0001
DateTime dt = new DateTime(ticks * 10000 + JavaScriptSerializer.DatetimeMinTimeTicks, DateTimeKind.Utc);
return dt;
}
else {
// If we failed to get a DateTime, treat it as a string
return DeserializeString();
}
}
private static bool IsNextElementArray(Nullable<Char> c) {
return c == '[';
}
private bool IsNextElementDateTime() {
String next = _s.MoveNext(DateTimePrefixLength);
if (next != null) {
_s.MovePrev(DateTimePrefixLength);
return String.Equals(next, DateTimePrefix, StringComparison.Ordinal);
}
return false;
}
private static bool IsNextElementObject(Nullable<Char> c) {
return c == '{';
}
private static bool IsNextElementString(Nullable<Char> c) {
return c == '"' || c == '\'';
}
}
}
|