Skip to content

Commit c0ba46c

Browse files
committed
CSHARP-2484: Support reading the new extended JSON formats.
1 parent 1a49c6e commit c0ba46c

File tree

5 files changed

+771
-23
lines changed

5 files changed

+771
-23
lines changed

src/MongoDB.Bson/IO/JsonReader.cs

Lines changed: 213 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Globalization;
1919
using System.IO;
2020
using System.Text.RegularExpressions;
21+
using MongoDB.Shared;
2122

2223
namespace MongoDB.Bson.IO
2324
{
@@ -923,6 +924,14 @@ private BsonReaderState GetNextState()
923924
}
924925
}
925926

927+
private bool IsValidBinaryDataSubTypeString(string value)
928+
{
929+
return
930+
value.Length >= 1 &&
931+
value.Length <= 2 &&
932+
HexUtils.IsValidHexString(value);
933+
}
934+
926935
private BsonValue ParseBinDataConstructor()
927936
{
928937
VerifyToken("(");
@@ -956,19 +965,117 @@ private BsonValue ParseBinDataExtendedJson()
956965
{
957966
VerifyToken(":");
958967

959-
var bytesToken = PopToken();
960-
if (bytesToken.Type != JsonTokenType.String)
968+
byte[] bytes;
969+
BsonBinarySubType subType;
970+
971+
var nextToken = PopToken();
972+
if (nextToken.Type == JsonTokenType.BeginObject)
961973
{
962-
var message = string.Format("JSON reader expected a string but found '{0}'.", bytesToken.Lexeme);
974+
ParseBinDataExtendedJsonCanonical(out bytes, out subType);
975+
}
976+
else
977+
{
978+
ParseBinDataExtendedJsonLegacy(nextToken, out bytes, out subType);
979+
}
980+
981+
VerifyToken("}");
982+
983+
GuidRepresentation guidRepresentation;
984+
switch (subType)
985+
{
986+
case BsonBinarySubType.UuidLegacy: guidRepresentation = _jsonReaderSettings.GuidRepresentation; break;
987+
case BsonBinarySubType.UuidStandard: guidRepresentation = GuidRepresentation.Standard; break;
988+
default: guidRepresentation = GuidRepresentation.Unspecified; break;
989+
}
990+
991+
return new BsonBinaryData(bytes, subType, guidRepresentation);
992+
}
993+
994+
private void ParseBinDataExtendedJsonCanonical(out byte[] bytes, out BsonBinarySubType subType)
995+
{
996+
string base64String = null;
997+
string subTypeString = null;
998+
999+
var nextToken = PopToken();
1000+
while (nextToken.Type != JsonTokenType.EndObject)
1001+
{
1002+
if (nextToken.Type != JsonTokenType.String && nextToken.Type != JsonTokenType.UnquotedString)
1003+
{
1004+
var message = string.Format("JSON reader expected a string but found '{0}'.", nextToken.Lexeme);
1005+
throw new FormatException(message);
1006+
}
1007+
var name = nextToken.StringValue;
1008+
1009+
nextToken = PopToken();
1010+
if (nextToken.Type != JsonTokenType.Colon)
1011+
{
1012+
var message = string.Format("JSON reader expected ':' but found '{0}'.", nextToken.Lexeme);
1013+
throw new FormatException(message);
1014+
}
1015+
1016+
nextToken = PopToken();
1017+
if (nextToken.Type != JsonTokenType.String)
1018+
{
1019+
var message = string.Format("JSON reader expected a string but found '{0}'.", nextToken.Lexeme);
1020+
throw new FormatException(message);
1021+
}
1022+
var value = nextToken.StringValue;
1023+
1024+
switch (name)
1025+
{
1026+
case "base64": base64String = value; break;
1027+
case "subType": subTypeString = value; break;
1028+
default:
1029+
var message = string.Format("JSON reader expected 'base64' or 'subType', but found '{0}'.", name);
1030+
throw new FormatException(message);
1031+
}
1032+
1033+
nextToken = PopToken();
1034+
if (nextToken.Type != JsonTokenType.Comma && nextToken.Type != JsonTokenType.EndObject)
1035+
{
1036+
var message = string.Format("JSON reader expected ',' or '}}' but found '{0}'.", nextToken.Lexeme);
1037+
throw new FormatException(message);
1038+
}
1039+
1040+
if (nextToken.Type == JsonTokenType.Comma)
1041+
{
1042+
nextToken = PopToken();
1043+
}
1044+
}
1045+
1046+
if (base64String == null)
1047+
{
1048+
var message = "JSON reader expected $binary to contain a 'base64' element.";
9631049
throw new FormatException(message);
9641050
}
965-
var bytes = Convert.FromBase64String(bytesToken.StringValue);
1051+
if (subTypeString == null)
1052+
{
1053+
var message = "JSON reader expected $binary to contain a 'subType' element.";
1054+
throw new FormatException(message);
1055+
}
1056+
if (!IsValidBinaryDataSubTypeString(subTypeString))
1057+
{
1058+
var message = string.Format("JSON reader expected subType to be a one or two digit hex string, but found '{0}'.", subTypeString);
1059+
throw new FormatException(message);
1060+
}
1061+
1062+
bytes = Convert.FromBase64String(base64String);
1063+
subType = (BsonBinarySubType)HexUtils.ParseInt32(subTypeString);
1064+
}
1065+
1066+
private void ParseBinDataExtendedJsonLegacy(JsonToken nextToken, out byte[] bytes, out BsonBinarySubType subType)
1067+
{
1068+
if (nextToken.Type != JsonTokenType.String)
1069+
{
1070+
var message = string.Format("JSON reader expected a string but found '{0}'.", nextToken.Lexeme);
1071+
throw new FormatException(message);
1072+
}
1073+
bytes = Convert.FromBase64String(nextToken.StringValue);
9661074

9671075
VerifyToken(",");
9681076
VerifyString("$type");
9691077
VerifyToken(":");
9701078

971-
BsonBinarySubType subType;
9721079
var subTypeToken = PopToken();
9731080
if (subTypeToken.Type == JsonTokenType.String)
9741081
{
@@ -983,18 +1090,6 @@ private BsonValue ParseBinDataExtendedJson()
9831090
var message = string.Format("JSON reader expected a string or integer but found '{0}'.", subTypeToken.Lexeme);
9841091
throw new FormatException(message);
9851092
}
986-
987-
VerifyToken("}");
988-
989-
GuidRepresentation guidRepresentation;
990-
switch (subType)
991-
{
992-
case BsonBinarySubType.UuidLegacy: guidRepresentation = _jsonReaderSettings.GuidRepresentation; break;
993-
case BsonBinarySubType.UuidStandard: guidRepresentation = GuidRepresentation.Standard; break;
994-
default: guidRepresentation = GuidRepresentation.Unspecified; break;
995-
}
996-
997-
return new BsonBinaryData(bytes, subType, guidRepresentation);
9981093
}
9991094

10001095
private BsonValue ParseHexDataConstructor()
@@ -1218,10 +1313,12 @@ private BsonType ParseExtendedJson()
12181313
case "$maxkey": case "$maxKey": _currentValue = ParseMaxKeyExtendedJson(); return BsonType.MaxKey;
12191314
case "$minkey": case "$minKey": _currentValue = ParseMinKeyExtendedJson(); return BsonType.MinKey;
12201315
case "$numberDecimal": _currentValue = ParseNumberDecimalExtendedJson(); return BsonType.Decimal128;
1316+
case "$numberDouble": _currentValue = ParseNumberDoubleExtendedJson(); return BsonType.Double;
12211317
case "$numberInt": _currentValue = ParseNumberIntExtendedJson(); return BsonType.Int32;
12221318
case "$numberLong": _currentValue = ParseNumberLongExtendedJson(); return BsonType.Int64;
12231319
case "$oid": _currentValue = ParseObjectIdExtendedJson(); return BsonType.ObjectId;
1224-
case "$regex": _currentValue = ParseRegularExpressionExtendedJson(); return BsonType.RegularExpression;
1320+
case "$regex": _currentValue = ParseRegularExpressionExtendedJsonLegacy(); return BsonType.RegularExpression;
1321+
case "$regularExpression": _currentValue = ParseRegularExpressionExtendedJsonCanonical(); return BsonType.RegularExpression;
12251322
case "$symbol": _currentValue = ParseSymbolExtendedJson(); return BsonType.Symbol;
12261323
case "$timestamp": _currentValue = ParseTimestampExtendedJson(); return BsonType.Timestamp;
12271324
case "$undefined": _currentValue = ParseUndefinedExtendedJson(); return BsonType.Undefined;
@@ -1549,6 +1646,31 @@ private BsonValue ParseNumberDecimalExtendedJson()
15491646
return (BsonDecimal128)value;
15501647
}
15511648

1649+
private BsonValue ParseNumberDoubleExtendedJson()
1650+
{
1651+
VerifyToken(":");
1652+
1653+
double value;
1654+
var valueToken = PopToken();
1655+
if (valueToken.IsNumber)
1656+
{
1657+
value = valueToken.DoubleValue;
1658+
}
1659+
else if (valueToken.Type == JsonTokenType.String)
1660+
{
1661+
value = JsonConvert.ToDouble(valueToken.StringValue);
1662+
}
1663+
else
1664+
{
1665+
var message = string.Format("JSON reader expected a number or numeric string but found '{0}'.", valueToken.Lexeme);
1666+
throw new FormatException(message);
1667+
}
1668+
1669+
VerifyToken("}");
1670+
1671+
return (BsonDouble)value;
1672+
}
1673+
15521674
private BsonValue ParseNumberIntExtendedJson()
15531675
{
15541676
VerifyToken(":");
@@ -1623,6 +1745,77 @@ private BsonValue ParseObjectIdExtendedJson()
16231745
return new BsonObjectId(ObjectId.Parse(valueToken.StringValue));
16241746
}
16251747

1748+
private BsonValue ParseRegularExpressionExtendedJsonCanonical()
1749+
{
1750+
VerifyToken(":");
1751+
VerifyToken("{");
1752+
1753+
string pattern = null;
1754+
string options = null;
1755+
1756+
var nextToken = PopToken();
1757+
while (nextToken.Type != JsonTokenType.EndObject)
1758+
{
1759+
if (nextToken.Type != JsonTokenType.String && nextToken.Type != JsonTokenType.UnquotedString)
1760+
{
1761+
var message = string.Format("JSON reader expected a string but found '{0}'.", nextToken.Lexeme);
1762+
throw new FormatException(message);
1763+
}
1764+
var name = nextToken.StringValue;
1765+
1766+
VerifyToken(":");
1767+
1768+
nextToken = PopToken();
1769+
if (nextToken.Type != JsonTokenType.String)
1770+
{
1771+
var message = string.Format("JSON reader expected a string but found '{0}'.", nextToken.Lexeme);
1772+
throw new FormatException(message);
1773+
}
1774+
var value = nextToken.StringValue;
1775+
1776+
switch (name)
1777+
{
1778+
case "pattern":
1779+
pattern = value;
1780+
break;
1781+
case "options":
1782+
options = value;
1783+
break;
1784+
default:
1785+
var message = string.Format("JSON reader expected 'pattern' or 'options' but found '{0}'.", nextToken.Lexeme);
1786+
throw new FormatException(message);
1787+
}
1788+
1789+
nextToken = PopToken();
1790+
if (nextToken.Type != JsonTokenType.Comma && nextToken.Type != JsonTokenType.EndObject)
1791+
{
1792+
var message = string.Format("JSON reader expected ',' or '}}' but found '{0}'.", nextToken.Lexeme);
1793+
throw new FormatException(message);
1794+
}
1795+
1796+
if (nextToken.Type == JsonTokenType.Comma)
1797+
{
1798+
nextToken = PopToken();
1799+
}
1800+
}
1801+
1802+
VerifyToken("}");
1803+
1804+
if (pattern == null)
1805+
{
1806+
var message = "JSON reader expected $regularExpression to contain a 'pattern' element.";
1807+
throw new FormatException(message);
1808+
}
1809+
1810+
if (options == null)
1811+
{
1812+
var message = "JSON reader expected $regularExpression to contain an 'options' element.";
1813+
throw new FormatException(message);
1814+
}
1815+
1816+
return new BsonRegularExpression(pattern, options);
1817+
}
1818+
16261819
private BsonValue ParseRegularExpressionConstructor()
16271820
{
16281821
VerifyToken("(");
@@ -1652,7 +1845,7 @@ private BsonValue ParseRegularExpressionConstructor()
16521845
return new BsonRegularExpression(patternToken.StringValue, options);
16531846
}
16541847

1655-
private BsonValue ParseRegularExpressionExtendedJson()
1848+
private BsonValue ParseRegularExpressionExtendedJsonLegacy()
16561849
{
16571850
VerifyToken(":");
16581851
var patternToken = PopToken();
@@ -1908,7 +2101,7 @@ private void VerifyString(string expectedString)
19082101
var token = PopToken();
19092102
if ((token.Type != JsonTokenType.String && token.Type != JsonTokenType.UnquotedString) || token.StringValue != expectedString)
19102103
{
1911-
var message = string.Format("JSON reader expected '{0}' but found '{1}'.", expectedString, token.StringValue);
2104+
var message = string.Format("JSON reader expected string '{0}' but found '{1}'.", expectedString, token.Lexeme);
19122105
throw new FormatException(message);
19132106
}
19142107
}

src/MongoDB.Bson/MongoDB.Bson.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454

5555
<ItemGroup>
5656
<Compile Include="..\MongoDB.Shared\Hasher.cs" Link="Hasher.cs" />
57+
<Compile Include="..\MongoDB.Shared\HexUtils.cs" Link="HexUtils.cs" />
5758
</ItemGroup>
5859

5960
<ItemGroup>

src/MongoDB.Shared/HexUtils.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* Copyright 2019-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Globalization;
17+
18+
namespace MongoDB.Shared
19+
{
20+
internal static class HexUtils
21+
{
22+
public static bool IsValidHexDigit(char c)
23+
{
24+
return
25+
c >= '0' && c <= '9' ||
26+
c >= 'a' && c <= 'f' ||
27+
c >= 'A' && c <= 'F';
28+
}
29+
30+
public static bool IsValidHexString(string s)
31+
{
32+
for (var i = 0; i < s.Length; i++)
33+
{
34+
if (!IsValidHexDigit(s[i]))
35+
{
36+
return false;
37+
}
38+
}
39+
40+
return true;
41+
}
42+
43+
public static int ParseInt32(string value)
44+
{
45+
return int.Parse(value, NumberStyles.HexNumber);
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)