[CSHARP-4820] C# driver doesn't work with IReadOnlyCollection<s> types Created: 25/Oct/23  Updated: 05/Feb/24

Status: In Code Review
Project: C# Driver
Component/s: Serialization
Affects Version/s: None
Fix Version/s: None

Type: Bug Priority: Minor - P4
Reporter: Ladan Nekuii Assignee: Robert Stam
Resolution: Unresolved Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: PNG File locals.png    
Case:
Documentation Changes Summary:

1. What would you like to communicate to the user about this feature?
2. Would you like the user to see examples of the syntax and/or executable code and its output?
3. Which versions of the driver/connector does this apply to?


 Description   

Summary

C# driver doesn't work with IReadOnlyCollection<s> types.

Customer is implementing CSFLE with C# driver, It is working with fields and generic IEnumerable<> types but recently a new feature tried to use IReadOnlyCollection<s> which should have been encrypted, but is not. They think the issue is somewhere in the C# driver as they are not getting called back into PropertyEncrypterBsonSerializer. Adding a .ToList() to the update operation ( after the Select()) fixes the issue.

Please provide the version of the driver. If applicable, please provide the MongoDB server version and topology (standalone, replica set, or sharded cluster).

MongoDB 7.0.2 (Replica Set)
net6.0 (also LINQv2)
C# Driver 2.18.0

How to Reproduce

Steps to reproduce. If possible, please include a Short, Self Contained, Correct (Compilable), Example.

Here is the sample code customer is using:
__

public class EncryptedPropertiesConvention : ConventionBase, IPostProcessingConvention
{
    public const string ConventionName = "Encryption";
    private readonly IKeyResolver _keyResolver;
    private readonly ConcurrentDictionary<SecretId, AsyncLazy<SymmetricKeyDto[]>> _keyCache;
    private readonly ConcurrentDictionary<Type, IBsonSerializer> _serializerCache;
 
    private EncryptedPropertiesConvention(IKeyResolver keyResolver)
    {
        this._keyResolver = keyResolver;
        this._keyCache = new ConcurrentDictionary<SecretId, AsyncLazy<SymmetricKeyDto[]>>();
        this._serializerCache = new ConcurrentDictionary<Type, IBsonSerializer>();
    }
 
    /// <summary>
    /// Applies a post processing modification to the class map.
    /// </summary>
    /// <param name="classMap">The class map.</param>
    public void PostProcess(BsonClassMap classMap)
    {
        foreach (var memberMap in classMap.DeclaredMemberMaps.Where(x => x.MemberInfo.GetCustomAttribute<SensitiveInformationPropertyAttribute>() != null))
        {
            var serializer = memberMap.GetSerializer();
            memberMap.SetSerializer(this.CreateSerializer(serializer));
        }
    }
 
    public static void Register(IKeyResolver keyResolver)
    {
        // Clear pre-existing encryption before activating new encryption
        ConventionRegistry.Remove(ConventionName);
 
        ConventionRegistry.Register(
            ConventionName,
            new ConventionPack
            {
                new EncryptedPropertiesConvention(keyResolver),
            },
            EncryptionHelper.HasPropertiesToEncrypt);
    }
 
    private IBsonSerializer CreateSerializer(IBsonSerializer serializer)
    {
        IBsonSerializer Create(Type t)
        {
            var serializerType = typeof(PropertyEncrypterBsonSerializer<>).MakeGenericType(serializer.ValueType);
            return (IBsonSerializer)Activator.CreateInstance(serializerType, serializer, this._keyResolver, this._keyCache);
        }
 
        return this._serializerCache.GetOrAdd(serializer.ValueType, Create);
    }
}`
 

For exemple, a document that has the following properties will only encrypt the IEnumerable<s> type:

__

public class MyMongoCollection 
{ 
    public Guid PrimaryKey { get; set; } 
    [SensitiveInformationProperty] public IEnumerable<string> UserEnumerable { get; set; } = new List<string>(); 
    [SensitiveInformationProperty] public IReadOnlyCollection<string> UserReadOnly { get; set; } = new List<string>(); 
    [SensitiveInformationProperty] public IList<string> UserList { get; set; } = new List<string>(); } 
 
    public class User 
    { 
        public User(string name, string email) 
        { 
            this.Name = name; 
            this.Email = email; 
        } 
 
        public string Name { get; set; } 
        public string Email { get; set; } } 
 
        private void SimpleUpsert(Guid primaryKey) 
        { 
            var filter = Builders<MyMongoCollection>.Filter.Eq(x => x.PrimaryKey, primaryKey); 
            User[] usersData = { new User("George", "george@email.com") }; 
            var update = Builders<MyMongoCollection>.Update 
                .SetOnInsert(d => d.PrimaryKey, primaryKey) 
                .Set(d => d.UserEnumerable, usersData?.Select(x => x.Email)) 
                .Set(d => d.UserReadOnly, usersData?.Select(x => x.Email)) 
                .Set(d => d.UserList, usersData?.Select(x => x.Email)); 
 
        // Fetch the collection with the driver 
        // then callls 
        // await collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); } 

 



 Comments   
Comment by Robert Stam [ 02/Feb/24 ]

I think we can set aside how the serialization is configured, as that's not part of the issue. The underlying issue is that a value is being serialized using the wrong serializer.

Looks like there are two pieces working together to result in this outcome.

The first piece is that the compiler is inferring the "wrong" type for `<TField>`in the following line of code:

updateBuilder.Set(d => d.ExternalInviteeEmails, invitees.Select(x => x.Email));

It is inferring the type of `<TField>` to be `IEnumerable<string>` instead of `IReadOnlyCollection<string>`.

The second piece is that the driver is trying to be helpful by making every possible attempt to serialize the value, preferably using the serializer configured for the `ExternalInviteeEmails` field. Using the serializer configured for the field requires converting the supplied value to the type of the field, but that's not possible in this case. A value of type `IEnumerable<string>` cannot be converted to `IReadOnlyCollection<string>`. Therefore the driver gives up on using the configured serializer and looks up one in the serializer registry instead.

Normally this works fine. But in your case it was important that we use the original serializer. I am creating a pull request that changes the driver to throw an exception if the supplied value cannot be converted to the type of the field.

In your scenario your code would still compile without the call to `ToList()`, but you will now get a runtime exception.

Comment by Ladan Nekuii [ 22/Jan/24 ]

Thanks Robert. Please let me know if you need any other information.

Comment by Robert Stam [ 16/Jan/24 ]

Thank you.

I will be back in the office tomorrow and will take another look.

Comment by Ladan Nekuii [ 16/Jan/24 ]

Here is the update:

If I look at your github I can see the try/catch statement in the same file linkname.
The repro is simple enough. It does not encrypt anything, it is a skeleton with the bootstrapping that the app performs. While running the sample on my computer I am able to get the exception thrown. Perhaps your dev runs with "Just my code" enabled in the debugging options? With that option ticked you won't break in the framework.

Here is the stack trace:

 

` System.Private.CoreLib.dll!System.Convert.ChangeType(object value, System.Type conversionType, System.IFormatProvider provider) Line 314 C# System.Private.CoreLib.dll!System.Convert.ChangeType(object value, System.Type conversionType) Line 288 C# > MongoDB.Driver.dll!MongoDB.Driver.FieldValueSerializerHelper.ConvertIfPossibleSerializer<System.Collections.Generic.IEnumerable<string>, System.Collections.Generic.IReadOnlyCollection<string>>.TryConvertValue(System.Collections.Generic.IEnumerable<string> value, out System.Collections.Generic.IReadOnlyCollection<string> convertedValue) Line 239 C# MongoDB.Driver.dll!MongoDB.Driver.FieldValueSerializerHelper.ConvertIfPossibleSerializer<System.Collections.Generic.IEnumerable<string>, System.Collections.Generic.IReadOnlyCollection<string>>.Serialize(MongoDB.Bson.Serialization.BsonSerializationContext context, MongoDB.Bson.Serialization.BsonSerializationArgs args, System.Collections.Generic.IEnumerable<string> value) Line 194 C# MongoDB.Bson.dll!MongoDB.Bson.Serialization.IBsonSerializerExtensions.Serialize<System.Collections.Generic.IEnumerable<string>>(MongoDB.Bson.Serialization.IBsonSerializer<System.Collections.Generic.IEnumerable<string>> serializer, MongoDB.Bson.Serialization.BsonSerializationContext context, System.Collections.Generic.IEnumerable<string> value) Line 75 C# MongoDB.Driver.dll!MongoDB.Driver.OperatorUpdateDefinition<Gravt.Tests.Integration.EncryptedPropertyDocument, System.Collections.Generic.IEnumerable<string>>.Render(MongoDB.Bson.Serialization.IBsonSerializer<Gravt.Tests.Integration.EncryptedPropertyDocument> documentSerializer, MongoDB.Bson.Serialization.IBsonSerializerRegistry serializerRegistry, MongoDB.Driver.Linq.LinqProvider linqProvider) Line 1518 C# MongoDB.Driver.dll!MongoDB.Driver.CombinedUpdateDefinition<Gravt.Tests.Integration.EncryptedPropertyDocument>.Render(MongoDB.Bson.Serialization.IBsonSerializer<Gravt.Tests.Integration.EncryptedPropertyDocument> documentSerializer, MongoDB.Bson.Serialization.IBsonSerializerRegistry serializerRegistry, MongoDB.Driver.Linq.LinqProvider linqProvider) Line 1412 C# MongoDB.Driver.dll!MongoDB.Driver.MongoCollectionImpl<Gravt.Tests.Integration.EncryptedPropertyDocument>.ConvertWriteModelToWriteRequest(MongoDB.Driver.WriteModel<Gravt.Tests.Integration.EncryptedPropertyDocument> model, int index) Line 751 C# System.Linq.dll!System.Linq.Enumerable.SelectIterator<MongoDB.Driver.WriteModel<Gravt.Tests.Integration.EncryptedPropertyDocument>, MongoDB.Driver.Core.Operations.WriteRequest>(System.Collections.Generic.IEnumerable<MongoDB.Driver.WriteModel<Gravt.Tests.Integration.EncryptedPropertyDocument>> source, System.Func<MongoDB.Driver.WriteModel<Gravt.Tests.Integration.EncryptedPropertyDocument>, int, MongoDB.Driver.Core.Operations.WriteRequest> selector) Line 89 C# System.Private.CoreLib.dll!System.Collections.Generic.List<MongoDB.Driver.Core.Operations.WriteRequest>.List(System.Collections.Generic.IEnumerable<MongoDB.Driver.Core.Operations.WriteRequest> collection) Line 85 C# System.Linq.dll!System.Linq.Enumerable.ToList<MongoDB.Driver.Core.Operations.WriteRequest>(System.Collections.Generic.IEnumerable<MongoDB.Driver.Core.Operations.WriteRequest> source) Line 29 C# MongoDB.Driver.Core.dll!MongoDB.Driver.Core.Operations.BulkMixedWriteOperation.BulkMixedWriteOperation(MongoDB.Driver.CollectionNamespace collectionNamespace, System.Collections.Generic.IEnumerable<MongoDB.Driver.Core.Operations.WriteRequest> requests, MongoDB.Driver.Core.WireProtocol.Messages.Encoders.MessageEncoderSettings messageEncoderSettings) Line 61 C# MongoDB.Driver.dll!MongoDB.Driver.MongoCollectionImpl<Gravt.Tests.Integration.EncryptedPropertyDocument>.CreateBulkWriteOperation(MongoDB.Driver.IClientSessionHandle session, System.Collections.Generic.IEnumerable<MongoDB.Driver.WriteModel<Gravt.Tests.Integration.EncryptedPropertyDocument>> requests, MongoDB.Driver.BulkWriteOptions options) Line 891 C# MongoDB.Driver.dll!MongoDB.Driver.MongoCollectionImpl<Gravt.Tests.Integration.EncryptedPropertyDocument>.BulkWriteAsync(MongoDB.Driver.IClientSessionHandle session, System.Collections.Generic.IEnumerable<MongoDB.Driver.WriteModel<Gravt.Tests.Integration.EncryptedPropertyDocument>> requests, MongoDB.Driver.BulkWriteOptions options, System.Threading.CancellationToken cancellationToken) Line 258 C# MongoDB.Driver.dll!MongoDB.Driver.MongoCollectionImpl<System.__Canon>.BulkWriteAsync.AnonymousMethod__0(MongoDB.Driver.IClientSessionHandle session) Line 240 C# MongoDB.Driver.dll!MongoDB.Driver.MongoCollectionImpl<Gravt.Tests.Integration.EncryptedPropertyDocument>.UsingImplicitSessionAsync<MongoDB.Driver.BulkWriteResult<Gravt.Tests.Integration.EncryptedPropertyDocument>>(System.Func<MongoDB.Driver.IClientSessionHandle, System.Threading.Tasks.Task<MongoDB.Driver.BulkWriteResult<Gravt.Tests.Integration.EncryptedPropertyDocument>>> funcAsync, System.Threading.CancellationToken cancellationToken) Line 1349 C# `

Here is a screenshot of his debugger with locals if that helps:


Please let me know if you need any other information. Thanks.

 

Comment by Robert Stam [ 11/Jan/24 ]

Perhaps a simple repro would be to use string fields and a "dummy" encrypting serializer that does something to that string (perhaps reverses the characters?).

It seems like this is probably a question about how to configure serialization so that the correct encrypting serializer gets used for a field. In order to troubleshoot that we don't need to actually encrypt the values, we just need to do something to the string so that we can verify that the values stored in the database have been "encrypted" as expected.

It's also not clear to me why the exception would be swallowed. I might need to see more repro code (enough to get the same exception myself).

Do you need to encrypt any kind of value? If so I assume that the encrypted values would be stored in the database as byte arrays?

Comment by Robert Stam [ 11/Jan/24 ]

> Which is swallowed by the driver here in FieldValueSerializerHelper

> {{try

{ convertedValue = (TTo)Convert.ChangeType(value, toType); return true; }

catch { }}}

I don't see any such line in FieldValueSerializerHelper.cs.

There is such a line in a different file. A full stack trace would help determine how we reached that line.

Comment by Ladan Nekuii [ 10/Jan/24 ]

>If the goal is to do field level encryption perhaps a better approach would be to use the built-in support for field level encryption.

We cannot use the CSFLE provided by Mongo because originally this product was built with EventStore as a primary data store. MongoDB was only used as a readmodel data store where the data can be thrown away and rebuilt from the event sourced streams. Because EventStore is immutable we cannot change the encryption keys. This mongo serializer technique handles our encryption that is the same used for EventStore. The company is making a lot of efforts to get a SOC2 certification and having this loophole where we could fail to encrypt data is a little bit worrisome.

>I need a better repro. And maybe a better explanation of exactly what is trying to be accomplished and how, and why you think that it is the way to do it.

I'll try to rework the sample I provided so it is more clear to you, but the behavior is a silent exception swallow, resulting in the client code thinking the encryption worked and thus serializing the unencrypted bytes as is. Converting the input field from IReadOnlyCollection to an IList fixes the problem, but a developer could accidentally fall into this trap in the future.

For the repro case. Nevermind the error you got when calling await this.ThisWillNotCallTheSerializer();

What is important is that the serializer is being called as you can see in the callstack. This is where we would perform our encryption of the field decorated with the SensitiveInformationPropertyAttribute. What is important is the other function. Just remove the above call in the test and execute it. With proper exception settings you should trap an exception like this:

System.InvalidCastException
HResult=0x80004002
Message=Object must implement IConvertible.
Source=System.Private.CoreLib
StackTrace:
at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider) in /_/src/libraries/System.Private.CoreLib/src/System/Convert.cs:line 322

Which is swallowed by the driver here in FieldValueSerializerHelper

{{try { convertedValue = (TTo)Convert.ChangeType(value, toType); return true; } catch { }}}

This code then gets presumably branched out of the serializer in the else statement here:

if (TryConvertValue(value, out convertedValue)) { args.NominalType = typeof(TTo); _serializer.Serialize(context, args, convertedValue); } else { var serializer = _serializerRegistry.GetSerializer<TFrom>(); serializer.Serialize(context, args, value); }

Let me know if this helps, or not.

Comment by Robert Stam [ 02/Jan/24 ]

I've also found more recent documentation on client side field level encryption here:

https://www.mongodb.com/docs/drivers/csharp/current/fundamentals/encrypt-fields/

Comment by Robert Stam [ 02/Jan/24 ]

If the goal is to do field level encryption perhaps a better approach would be to use the built-in support for field level encryption:

See:

https://mongodb.github.io/mongo-csharp-driver/2.18/reference/driver/crud/client_side_encryption/

Comment by Robert Stam [ 02/Jan/24 ]

I need a better repro. And maybe a better explanation of exactly what is trying to be accomplished and how, and why you think that it is the way to do it.

When I run the repro provided above I simply get this exception:

System.NotSupportedException : Values of type 'IReadOnlyCollection<String>' cannot be serialized using a serializer of type 'PropertyEncrypterBsonSerializer<IReadOnlyCollection<String>>'. 

Comment by Ladan Nekuii [ 02/Jan/24 ]

Thanks for the update.

In their case it silently fails and the codepath continues. The end result on their end is that the caller thinks that their encryption succeeded and writes the document in clear text.

Please let me know if you need any other information. Thanks.

Comment by Robert Stam [ 26/Dec/23 ]

In my attempts to reproduce this I get an exception from the PropertyEncrypterBsonSerializer methods. The Deserialize and Serialize methods call the base methods which in turn throw exceptions.

Comment by Robert Stam [ 26/Dec/23 ]

Thank you for the code to reproduce this with.

Can you also let me know exactly how it is failing for you? Is it throwing an exception? 

Comment by Ladan Nekuii [ 21/Dec/23 ]

Hi robert@mongodb.com, Here is a reproduction test using xunit:

using System.Collections;
using System.Reflection;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using Xunit;
 
namespace Gravt.Tests.Integration;
 
public class SensitiveInformationTest
{
    private readonly IMongoCollection<EncryptedPropertyDocument> _mongoCollection;
 
    public SensitiveInformationTest()
    {
        var mongoUrl = new MongoUrl("mongodb://localhost:27017/");
        var settings = MongoClientSettings.FromUrl(mongoUrl);
 
#pragma warning disable 618
        settings.GuidRepresentation = GuidRepresentation.Standard;
#pragma warning restore 618
 
        ConventionRegistry.Register(
            "Encryption",
            new ConventionPack
            {
                new EncryptedPropertiesConvention(),
            },
            EncryptedPropertiesConvention.HasPropertiesToEncrypt);
 
        var mongoClient = new MongoClient(settings);
 
        this._mongoCollection = mongoClient.GetDatabase("default").GetCollection<EncryptedPropertyDocument>("EncryptedPropertyDocument");
    }
 
    [Fact]
    public async Task Attempt_Encrypted_Serialization()
    {
        await this.ThisWillCallTheSerializer();
        await this.ThisWillNotCallTheSerializer();
    }
 
    private async Task ThisWillNotCallTheSerializer()
    {
        var tenantId = Guid.NewGuid();
        var invitees = new User[] { new User("test1@email.com"), new User("test2@email.com"), new User("test3@email.com"), new User("test4@email.com") };
 
        var filter = Builders<EncryptedPropertyDocument>.Filter.Eq(x => x.TenantId, tenantId);
        var update = Builders<EncryptedPropertyDocument>.Update
            .SetOnInsert(d => d.TenantId, tenantId)
            .Set(d => d.ExternalInviteeEmails, invitees.Select(x => x.Email));
 
        await this._mongoCollection.UpdateOneAsync(filter, update, new UpdateOptions() { IsUpsert = true });
    }
 
    private async Task ThisWillCallTheSerializer()
    {
        var tenantId = Guid.NewGuid();
        var invitees = new User[] { new User("test1@email.com"), new User("test2@email.com"), new User("test3@email.com"), new User("test4@email.com") };
 
        var filter = Builders<EncryptedPropertyDocument>.Filter.Eq(x => x.TenantId, tenantId);
        var update = Builders<EncryptedPropertyDocument>.Update
            .SetOnInsert(d => d.TenantId, tenantId)
            .Set(d => d.ExternalInviteeEmails, invitees.Select(x => x.Email).ToList());
 
        await this._mongoCollection.UpdateOneAsync(filter, update, new UpdateOptions() { IsUpsert = true });
    }
 
    private record User(string Email);
}
 
public class EncryptedPropertiesConvention : ConventionBase, IPostProcessingConvention
{
    public void PostProcess(BsonClassMap classMap)
    {
        foreach (var memberMap in classMap.DeclaredMemberMaps.Where(x => x.MemberInfo.GetCustomAttribute<SensitiveInformationPropertyAttribute>() != null))
        {
            var serializer = memberMap.GetSerializer();
            memberMap.SetSerializer(this.CreateSerializer(serializer));
        }
    }
 
    public static bool HasPropertiesToEncrypt(Type type)
    {
        foreach (var property in type.GetProperties())
        {
            if (property.IsDefined(typeof(SensitiveInformationPropertyAttribute), true))
            {
                return true;
            }
 
            var propertyType = property.PropertyType;
            if (!propertyType.IsValueType && propertyType != typeof(string))
            {
                if (ReflectionHelper.IsEnumerable(propertyType))
                {
                    propertyType = ReflectionHelper.GetEnumerableItemType(propertyType);
 
                    if (propertyType == null)
                    {
                        continue;
                    }
                }
 
                if (HasPropertiesToEncrypt(propertyType))
                {
                    return true;
                }
            }
        }
 
        return false;
    }
 
    private IBsonSerializer CreateSerializer(IBsonSerializer serializer)
    {
        var serializerType = typeof(PropertyEncrypterBsonSerializer<>).MakeGenericType(serializer.ValueType);
        return (IBsonSerializer)Activator.CreateInstance(serializerType);
    }
}
 
internal sealed class PropertyEncrypterBsonSerializer<T> : SerializerBase<T>
{
    public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        return base.Deserialize(context, args);
    }
 
    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value)
    {
        base.Serialize(context, args, value);
    }
}
 
internal sealed class EncryptedPropertyDocument
{
    [BsonId]
    public ObjectId Id { get; set; }
 
    public Guid TenantId { get; set; }
 
    [SensitiveInformationProperty]
    public IReadOnlyCollection<string> ExternalInviteeEmails { get; set; } = new List<string>();
}
 
[AttributeUsage(AttributeTargets.Property)]
internal sealed class SensitiveInformationPropertyAttribute : Attribute
{
}
 
internal static class ReflectionHelper
{
    public static bool IsEnumerable(Type type)
    {
        return typeof(IEnumerable).IsAssignableFrom(type);
    }
 
    public static bool IsDictionary(Type type)
    {
        var cossin = GetEnumerableItemType(type);
 
        if (cossin == null)
        {
            return false;
        }
 
        return cossin.IsGenericType && cossin.GetGenericTypeDefinition() == typeof(KeyValuePair<,>);
    }
 
    public static Type GetEnumerableItemType(Type type)
    {
        if (IsEnumerable(type))
        {
            if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            {
                return type.GetGenericArguments()[0];
            }
 
            var interfaces = type.GetInterfaces();
            var enumerableType = interfaces.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
 
            if (enumerableType == null)
            {
                throw new NotSupportedException($"The current type {type} is not supported.");
            }
 
            return enumerableType.GetGenericArguments()[0];
        }
 
        return null;
    }
} 

Please let me know if you need anything else. Thanks.

Comment by PM Bot [ 15/Nov/23 ]

There hasn't been any recent activity on this ticket, so we're resolving it. Thanks for reaching out! Please feel free to reopen this ticket if you're still experiencing the issue, and add a comment if you're able to provide more information.

Comment by Carl Tremblay [ 07/Nov/23 ]

We're in the middle of something, I will take a look in the near future.

Comment by PM Bot [ 07/Nov/23 ]

Hi ladan.nekuii@mongodb.com! CSHARP-4820 is awaiting your response.

If this is still an issue for you, please open Jira to review the latest status and provide your feedback. Thanks!

Comment by Robert Stam [ 30/Oct/23 ]

I reformatted the code in the description to make it readable but discovered that it ends abruptly and some of the code is missing.

Can you take a look and provide the missing code?

Generated at Wed Feb 07 21:49:28 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.