diff --git a/Microsoft.Azure.Cosmos.Encryption.Custom/src/Microsoft.Azure.Cosmos.Encryption.Custom.csproj b/Microsoft.Azure.Cosmos.Encryption.Custom/src/Microsoft.Azure.Cosmos.Encryption.Custom.csproj index 6f492824b6..cca4d719bf 100644 --- a/Microsoft.Azure.Cosmos.Encryption.Custom/src/Microsoft.Azure.Cosmos.Encryption.Custom.csproj +++ b/Microsoft.Azure.Cosmos.Encryption.Custom/src/Microsoft.Azure.Cosmos.Encryption.Custom.csproj @@ -38,11 +38,11 @@ - + - + diff --git a/Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/JObjectSqlSerializer.Preview.cs b/Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/JObjectSqlSerializer.Preview.cs index e5edc3ade2..aadbe485f3 100644 --- a/Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/JObjectSqlSerializer.Preview.cs +++ b/Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/JObjectSqlSerializer.Preview.cs @@ -49,10 +49,10 @@ internal virtual (TypeMarker typeMarker, byte[] serializedBytes, int serializedB (buffer, length) = SerializeString(propertyValue.ToObject()); return (TypeMarker.String, buffer, length); case JTokenType.Array: - (buffer, length) = SerializeString(propertyValue.ToString()); + (buffer, length) = SerializeString(propertyValue.ToString(Formatting.None)); return (TypeMarker.Array, buffer, length); case JTokenType.Object: - (buffer, length) = SerializeString(propertyValue.ToString()); + (buffer, length) = SerializeString(propertyValue.ToString(Formatting.None)); return (TypeMarker.Object, buffer, length); default: throw new InvalidOperationException($" Invalid or Unsupported Data Type Passed : {propertyValue.Type}"); diff --git a/Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/JsonNodeSqlSerializer.Preview.cs b/Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/JsonNodeSqlSerializer.Preview.cs new file mode 100644 index 0000000000..6a607a3730 --- /dev/null +++ b/Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/JsonNodeSqlSerializer.Preview.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +#if ENCRYPTION_CUSTOM_PREVIEW && NET8_0_OR_GREATER +namespace Microsoft.Azure.Cosmos.Encryption.Custom.Transformation +{ + using System; + using System.Diagnostics; + using System.Text.Json; + using System.Text.Json.Nodes; + using Microsoft.Data.Encryption.Cryptography.Serializers; + + internal class JsonNodeSqlSerializer + { + private static readonly SqlBitSerializer SqlBoolSerializer = new (); + private static readonly SqlFloatSerializer SqlDoubleSerializer = new (); + private static readonly SqlBigIntSerializer SqlLongSerializer = new (); + + // UTF-8 encoding. + private static readonly SqlVarCharSerializer SqlVarCharSerializer = new (size: -1, codePageCharacterEncoding: 65001); + +#pragma warning disable SA1101 // Prefix local calls with this - false positive on SerializeFixed + internal virtual (TypeMarker typeMarker, byte[] serializedBytes, int serializedBytesCount) Serialize(JsonNode propertyValue, ArrayPoolManager arrayPoolManager) + { + byte[] buffer; + int length; + + if (propertyValue == null) + { + return (TypeMarker.Null, null, -1); + } + + switch (propertyValue.GetValueKind()) + { + case JsonValueKind.Undefined: + Debug.Assert(false, "Undefined value cannot be in the JSON"); + return (default, null, -1); + case JsonValueKind.Null: + Debug.Assert(false, "Null type should have been handled by caller"); + return (TypeMarker.Null, null, -1); + case JsonValueKind.True: + (buffer, length) = SerializeFixed(SqlBoolSerializer, true); + return (TypeMarker.Boolean, buffer, length); + case JsonValueKind.False: + (buffer, length) = SerializeFixed(SqlBoolSerializer, false); + return (TypeMarker.Boolean, buffer, length); + case JsonValueKind.Number: + if (long.TryParse(propertyValue.ToJsonString(), out long longValue)) + { + (buffer, length) = SerializeFixed(SqlLongSerializer, longValue); + return (TypeMarker.Long, buffer, length); + } + else if (double.TryParse(propertyValue.ToJsonString(), out double doubleValue)) + { + (buffer, length) = SerializeFixed(SqlDoubleSerializer, doubleValue); + return (TypeMarker.Double, buffer, length); + } + else + { + throw new InvalidOperationException("Unsupported Number type"); + } + + case JsonValueKind.String: + (buffer, length) = SerializeString(propertyValue.GetValue()); + return (TypeMarker.String, buffer, length); + case JsonValueKind.Array: + (buffer, length) = SerializeString(propertyValue.ToJsonString()); + return (TypeMarker.Array, buffer, length); + case JsonValueKind.Object: + (buffer, length) = SerializeString(propertyValue.ToJsonString()); + return (TypeMarker.Object, buffer, length); + default: + throw new InvalidOperationException($" Invalid or Unsupported Data Type Passed : {propertyValue.GetValueKind()}"); + } + + (byte[], int) SerializeFixed(IFixedSizeSerializer serializer, T value) + { + byte[] buffer = arrayPoolManager.Rent(serializer.GetSerializedMaxByteCount()); + int length = serializer.Serialize(value, buffer); + return (buffer, length); + } + + (byte[], int) SerializeString(string value) + { + byte[] buffer = arrayPoolManager.Rent(SqlVarCharSerializer.GetSerializedMaxByteCount(value.Length)); + int length = SqlVarCharSerializer.Serialize(value, buffer); + return (buffer, length); + } + } + } +} +#endif \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests.csproj b/Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests.csproj index 7e1f7d48fe..8dbf3f22fc 100644 --- a/Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests.csproj +++ b/Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests.csproj @@ -9,6 +9,7 @@ false Microsoft.Azure.Cosmos.Encryption.Tests $(LangVersion) + $(DefineConstants);ENCRYPTION_CUSTOM_PREVIEW diff --git a/Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Transformation/JsonNodeSqlSerializerTests.cs b/Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Transformation/JsonNodeSqlSerializerTests.cs new file mode 100644 index 0000000000..deb6bb7b35 --- /dev/null +++ b/Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Transformation/JsonNodeSqlSerializerTests.cs @@ -0,0 +1,92 @@ +#if ENCRYPTION_CUSTOM_PREVIEW && NET8_0_OR_GREATER + +namespace Microsoft.Azure.Cosmos.Encryption.Tests.Transformation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.Json.Nodes; + using Microsoft.Azure.Cosmos.Encryption.Custom; + using Microsoft.Azure.Cosmos.Encryption.Custom.Transformation; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json.Linq; + + [TestClass] + public class JsonNodeSqlSerializerTests + { + private static ArrayPoolManager _poolManager; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + _ = context; + _poolManager = new ArrayPoolManager(); + } + + [TestMethod] + [DynamicData(nameof(SerializationSamples))] + public void Serialize_SupportedValue(JsonNode testNode, byte expectedType, byte[] expectedBytes, int expectedLength) + { + JsonNodeSqlSerializer serializer = new(); + + (TypeMarker serializedType, byte[] serializedBytes, int serializedBytesCount) = serializer.Serialize(testNode, _poolManager); + + Assert.AreEqual((TypeMarker)expectedType, serializedType); + Assert.AreEqual(expectedLength, serializedBytesCount); + if (expectedLength == -1) + { + Assert.IsTrue(serializedBytes == null); + } + else + { + Assert.IsTrue(expectedBytes.SequenceEqual(serializedBytes.AsSpan(0, serializedBytesCount).ToArray())); + } + } + + public static IEnumerable SerializationSamples + { + get + { + List values = new() + { + new object[] {JsonValue.Create((string)null), (byte)TypeMarker.Null, null, -1 }, + new object[] {JsonValue.Create(true), (byte)TypeMarker.Boolean, GetNewtonsoftValueEquivalent(true), 8}, + new object[] {JsonValue.Create(false), (byte)TypeMarker.Boolean, GetNewtonsoftValueEquivalent(false), 8}, + new object[] {JsonValue.Create(192), (byte)TypeMarker.Long, GetNewtonsoftValueEquivalent(192), 8}, + new object[] {JsonValue.Create(192.5), (byte)TypeMarker.Double, GetNewtonsoftValueEquivalent(192.5), 8}, + new object[] {JsonValue.Create(testString), (byte)TypeMarker.String, GetNewtonsoftValueEquivalent(testString), 11}, + new object[] {JsonValue.Create(testArray), (byte)TypeMarker.Array, GetNewtonsoftValueEquivalent(testArray), 10}, + new object[] {JsonValue.Create(testClass), (byte)TypeMarker.Object, GetNewtonsoftValueEquivalent(testClass), 33} + }; + + return values; + } + } + + private static readonly string testString = "Hello world"; + private static readonly int[] testArray = new[] {10, 18, 19}; + private static readonly TestClass testClass = new() { SomeInt = 1, SomeString = "asdf" }; + + private class TestClass + { + public int SomeInt { get; set; } + public string SomeString { get; set; } + } + + private static byte[] GetNewtonsoftValueEquivalent(T value) + { + JObjectSqlSerializer serializer = new (); + JToken token = value switch + { + int[] => new JArray(value), + TestClass => JObject.FromObject(value), + _ => new JValue(value), + }; + (TypeMarker _, byte[] bytes, int lenght) = serializer.Serialize(token, _poolManager); + return bytes.AsSpan(0, lenght).ToArray(); + } + + } +} + +#endif \ No newline at end of file