Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and enhance StringConverter() to handle more special characters #361

Merged
merged 17 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion nanoFramework.Json.Test/Converters/StringConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ namespace nanoFramework.Json.Test.Converters
public class StringConverterTests
{
[TestMethod]
[DataRow("\"TestJson\"", "\"TestJson\"")]
[DataRow("\"a\"", "a")]
[DataRow("\"TestJson\"", "TestJson")]
[DataRow("TestJson1", "TestJson1")]
[DataRow("\"Test / solidus\"", "Test / solidus")]
public void StringConverter_ToType_ShouldReturnValidData(string value, string expectedValue)
{
var converter = new Json.Converters.StringConverter();
Expand All @@ -31,13 +33,54 @@ public void StringConverter_ToType_ShouldReturnStringEmptyForNull()
}

[TestMethod]
[DataRow("a", "\"a\"")]
[DataRow("\"TestJson\"", "\"\\\"TestJson\\\"\"")]
[DataRow("TestJson2", "\"TestJson2\"")]
[DataRow("Test / solidus", "\"Test / solidus\"")]
public void StringConverter_ToJson_Should_ReturnValidData(string value, string expectedValue)
{
var converter = new Json.Converters.StringConverter();
var convertedValue = converter.ToJson(value);

Assert.AreEqual(expectedValue, convertedValue);
}

[TestMethod]
[DataRow("Text\\1", "Text\\1")] // Backslash
[DataRow("Text\b1", "Text\b1")] // Backspace
[DataRow("Text\f1", "Text\f1")] // FormFeed
[DataRow("Text\r1", "Text\r1")] // CarriageReturn
[DataRow("Text\"1", "Text\"1")] // DoubleQuote
[DataRow("Text\n1", "Text\n1")] // Newline
[DataRow("Text\t1", "Text\t1")] // Tab
[DataRow("['Text3', 1]", "['Text3', 1]")] // Array
[DataRow("{\"Text1\" : \"/Text1/\"}", "{\"Text1\" : \"/Text1/\"}")] // Json
[DataRow("ä", "ä")] // Unicode
[DataRow("\"I:\\\\nano\\\\rpath\\\\to\"", "I:\\nano\\rpath\\to")]
public void StringConverter_ToType_Should_HandleSpecialCharacters(string value, string expectedValue)
{
var converter = new Json.Converters.StringConverter();
var convertedValue = (string)converter.ToType(value);

Assert.AreEqual(expectedValue, convertedValue);
}

[TestMethod]
[DataRow("Text\\1", "\"Text\\\\1\"")] // Backslash
[DataRow("Text\b1", "\"Text\\b1\"")] // Backspace
[DataRow("Text\f1", "\"Text\\f1\"")] // FormFeed
[DataRow("Text\r1", "\"Text\\r1\"")] // CarriageReturn
[DataRow("Text\"1", "\"Text\\\"1\"")] // DoubleQuote
[DataRow("Text\n1", "\"Text\\n1\"")] // Newline
[DataRow("Text\t1", "\"Text\\t1\"")] // Tab
[DataRow("ä", "\"ä\"")] // Unicode
[DataRow("I:\\nano\\rpath\\to", "\"I:\\\\nano\\\\rpath\\\\to\"")]
public void StringConverter_ToJson_Should_HandleSpecialCharacters(string value, string expectedValue)
{
var converter = new Json.Converters.StringConverter();
var convertedValue = converter.ToJson(value);

Assert.AreEqual(expectedValue, convertedValue);
}
}
}
58 changes: 54 additions & 4 deletions nanoFramework.Json.Test/JsonSerializerOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,18 +375,68 @@ public class ThingWithString
}

[TestMethod]
public void Can_serialize_and_deserialize_escaped_string()
[DataRow("a")]
[DataRow("1")]
[DataRow("\t")]
[DataRow("Testing / solidus")]
[DataRow("Testing solidus")]
[DataRow("Some string with \" that needs escaping")]
[DataRow("Quotes in a \"string\".")]
[DataRow("Escaped last character \n")]
[DataRow("I:\\Nano\\rApp\\app.pe")] // Backslash
[DataRow("Tab \t in a string \t")]
[DataRow("Newline \n in a string \n")]
[DataRow("LineFeed \f in a string \f")]
[DataRow("CarriageReturn \r in a string \r")]
[DataRow("Backspace \b in a string \b")]
[DataRow("TestString")]
[DataRow("\"TestString\"")]
public void Can_serialize_and_deserialize_object_containing_string_with_escaped_characters(string testValue)
{
var thing = new ThingWithString
{
Value = "Some string with \" that needs escaping"
Value = testValue
};

Console.WriteLine("Original: " + testValue);

var serialized = JsonConvert.SerializeObject(thing);
Console.WriteLine("Serialized: " + serialized);

var deserialized = (ThingWithString)JsonConvert.DeserializeObject(serialized, typeof(ThingWithString));
Console.WriteLine("Deserialized: " + deserialized.Value);

Assert.AreEqual(thing.Value, deserialized.Value);
}

[TestMethod]
[DataRow("a")]
[DataRow("\t")]
[DataRow("Testing / solidus")]
[DataRow("Testing solidus")]
[DataRow("Quotes in a \"string\".")]
[DataRow("Escaped last character \n")]
[DataRow("I:\\Nano\\rApp\\app.pe")] // Backslash
[DataRow("Tab \t in a string \ta")]
[DataRow("Newline \n in a string \na")]
[DataRow("LineFeed \f in a string \fa")]
[DataRow("CarriageReturn \r in a string \ra")]
[DataRow("Backspace \b in a string \ba")]
[DataRow("TestString")]
[DataRow("\"TestString\"")]
public void Can_serialize_and_deserialize_string_with_escaped_characters(string testValue)
{
Console.WriteLine("Original: " + testValue);

var serialized = JsonConvert.SerializeObject(testValue);
Console.WriteLine("Serialized: " + serialized);

var deserialized = (string)JsonConvert.DeserializeObject(serialized, typeof(string));
Console.WriteLine("Deserialized: " + deserialized);

Assert.AreEqual(testValue, deserialized);
}

[TestMethod]
public void Can_serialize_and_deserialize_complex_object()
{
Expand Down Expand Up @@ -1037,8 +1087,8 @@ public void CanDeserializeInvocationReceiveMessage_05()

string arg1 = (string)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(dserResult.arguments[1]), typeof(string));

Assert.AreEqual(arg0, "\"I_am_a_string\"", $"arg0 has unexpected value: {arg0}");
Assert.AreEqual(arg1, "\"I_am_another_string\"", $"arg1 has unexpected value: {arg1}");
Assert.AreEqual(arg0, "I_am_a_string", $"arg0 has unexpected value: {arg0}");
Assert.AreEqual(arg1, "I_am_another_string", $"arg1 has unexpected value: {arg1}");
}

[TestMethod]
Expand Down
39 changes: 30 additions & 9 deletions nanoFramework.Json/Converters/StringConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ internal sealed class StringConverter : IConverter
{
{'\n', 'n'},
{'\r', 'r'},
{'\b', 'b' },
{'\f', 'f' },
{'\t', 't' },
{'\\', '\\' },
{'\"', '"' }
};

Expand Down Expand Up @@ -92,30 +96,47 @@ public object ToType(object value)
}

var sourceString = value.ToString();

// String by default has escaped \" at beggining and end, just remove them
// if they have already been removed, string has likely already been deserialized,
// and if so, then we just return it.
if (!sourceString.StartsWith("\"") && !sourceString.EndsWith("\""))
{
return sourceString;
}
string resultString = sourceString.Substring(1, sourceString.Length - 2);

// No characters to escape so we short circuit the character loop
if (!StringContainsCharactersToEscape(sourceString, true))
{
return value;
return resultString;
}

//String by default has escaped \" at beggining and end, just remove them
var resultString = sourceString.Substring(1, sourceString.Length - 2);
var newString = new StringBuilder();
//Last character can not be escaped, because it's last one
for (int i = 0; i < resultString.Length - 1; i++)
{
var curChar = resultString[i];
var nextChar = resultString[i + 1];

if (curChar == '\\')
if (curChar != '\\')
{
var charToAppend = GetEscapableCharKeyBasedOnValue(nextChar);
newString.Append(charToAppend);
i++;
newString.Append(curChar);
continue;
}
newString.Append(curChar);

var charToAppend = GetEscapableCharKeyBasedOnValue(nextChar);
newString.Append(charToAppend);
i++;

// If the end of the string is an escapped character, return the string
if (i == resultString.Length - 1)
{
return newString.ToString();
}
}
//Append last character skkiped by loop

//Append last character skipped by loop
newString.Append(resultString[resultString.Length - 1]);
return newString.ToString();
}
Expand Down
20 changes: 20 additions & 0 deletions nanoFramework.Json/JsonConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,18 @@ private static LexToken GetNextTokenInternal(ref int jsonPos, ref byte[] jsonByt
ch = '\n';
break;

case 'b':
ch = '\b';
break;

case 'f':
ch = '\f';
break;

case '\\':
ch = '\\';
break;

case 'u':
unicodeEncoded = true;
break;
Expand Down Expand Up @@ -1239,6 +1251,14 @@ private static LexToken GetNextTokenInternal(ref int jsonPos, ref byte[] jsonByt

var stringValue = sb.ToString();

// This adds an extra set of quotes since an extra set is removed during de-serialization
if (ch == '"' && stringValue.StartsWith("\""))
{
sb.Insert(0, "\"", 1);
sb.Append("\"");
stringValue = sb.ToString();
}

if (DateTimeExtensions.ConvertFromString(stringValue, out _))
{
return new LexToken() { TType = TokenType.Date, TValue = stringValue };
Expand Down