Skip to content

Commit

Permalink
[Client encryption]: Add JsonNodeSqlSerializer (#4779)
Browse files Browse the repository at this point in the history
# Pull Request Template

## Description

Initial commit for JsonNode on Encryption path. This is currently not
executed on any production/preview path. Depends on JsonNode features
available from System.Text.Json 8.0+.

- Adds JsonNodeSqlSerializer
- JObjectSqlSerializer now doesn't format inner serialized
JArrays/JObjects (with line breaks/indentations)

To be processed after #4766 

## Type of change

Please delete options that are not relevant.

- [] New feature (non-breaking change which adds functionality)

## Closing issues

Contributes to #4678

---------

Co-authored-by: Juraj Blazek <jublazek@microsoft.com>
Co-authored-by: juraj-blazek <53177060+juraj-blazek@users.noreply.github.com>
Co-authored-by: Santosh Kulkarni <66682828+kr-santosh@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 10, 2024
1 parent b5d7da0 commit 4f515bb
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@
</ItemGroup>

<ItemGroup Condition=" '$(IsPreview)' != 'True' ">
<PackageReference Include="Microsoft.Data.Encryption.Cryptography" Version="1.2.0" />
<PackageReference Include="Microsoft.Data.Encryption.Cryptography" Version="1.2.0" />
</ItemGroup>

<ItemGroup Condition=" '$(IsPreview)' == 'True' ">
<PackageReference Include="Microsoft.Data.Encryption.Cryptography" Version="2.0.0-pre007" />
<PackageReference Include="Microsoft.Data.Encryption.Cryptography" Version="2.0.0-pre007" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ internal virtual (TypeMarker typeMarker, byte[] serializedBytes, int serializedB
(buffer, length) = SerializeString(propertyValue.ToObject<string>());
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}");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>());
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<T>(IFixedSizeSerializer<T> 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RootNamespace>Microsoft.Azure.Cosmos.Encryption.Tests</RootNamespace>
<LangVersion>$(LangVersion)</LangVersion>
<DefineConstants Condition=" '$(IsPreview)' == 'True' ">$(DefineConstants);ENCRYPTION_CUSTOM_PREVIEW</DefineConstants>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<object[]> SerializationSamples
{
get
{
List<object[]> 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>(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

0 comments on commit 4f515bb

Please sign in to comment.