about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2024-09-17 04:28:20 +0200
committerRory& <root@rory.gay>2024-09-17 04:28:20 +0200
commit92b139e41afd41f3bd3987c14208966d020a948b (patch)
treee21c6ad5bb0c833a8151c9b60bc4f1de22fa5c8d /LibMatrix
parentMerge remote-tracking branch 'origin/dev/project-cleanup' (diff)
parentWorking json canonicalisation (diff)
downloadLibMatrix-92b139e41afd41f3bd3987c14208966d020a948b.tar.xz
Merge remote-tracking branch 'origin/dev/e2ee{canonical-json}'
Diffstat (limited to 'LibMatrix')
-rw-r--r--LibMatrix/Extensions/CanonicalJsonSerializer.cs91
-rw-r--r--LibMatrix/Extensions/UnicodeJsonEncoder.cs173
-rw-r--r--LibMatrix/LibMatrix.csproj1
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs6
-rw-r--r--LibMatrix/RoomTypes/SpaceRoom.cs4
5 files changed, 271 insertions, 4 deletions
diff --git a/LibMatrix/Extensions/CanonicalJsonSerializer.cs b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
new file mode 100644
index 0000000..a6fbcf4
--- /dev/null
+++ b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
@@ -0,0 +1,91 @@
+using System.Collections.Frozen;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using System.Text.Unicode;
+using ArcaneLibs.Extensions;
+
+namespace LibMatrix.Extensions;
+
+public static class CanonicalJsonSerializer {
+    // TODO: Alphabetise dictionaries
+    private static JsonSerializerOptions _options => new() {
+        WriteIndented = false,
+        Encoder = UnicodeJsonEncoder.Singleton,
+    };
+
+    private static readonly FrozenSet<PropertyInfo> JsonSerializerOptionsProperties = typeof(JsonSerializerOptions)
+        .GetProperties(BindingFlags.Public | BindingFlags.Instance)
+        .Where(x => x.SetMethod != null && x.GetMethod != null)
+        .ToFrozenSet();
+
+    private static JsonSerializerOptions MergeOptions(JsonSerializerOptions? inputOptions) {
+        var newOptions = _options;
+        if (inputOptions == null)
+            return newOptions;
+        
+        foreach (var property in JsonSerializerOptionsProperties) {
+            if(property.Name == nameof(JsonSerializerOptions.Encoder))
+                continue;
+            if (property.Name == nameof(JsonSerializerOptions.WriteIndented))
+                continue;
+                
+            var value = property.GetValue(inputOptions);
+            // if (value == null)
+                // continue;
+            property.SetValue(newOptions, value);
+        }
+
+        return newOptions;
+    }
+
+#region STJ API
+
+    public static String Serialize<TValue>(TValue value, JsonSerializerOptions? options = null) {
+        var newOptions = MergeOptions(options);
+
+        return System.Text.Json.JsonSerializer.SerializeToNode(value, options) // We want to allow passing custom converters for eg. double/float -> string here...
+            .SortProperties()!
+            .CanonicalizeNumbers()!
+            .ToJsonString(newOptions);
+        
+            
+        // System.Text.Json.JsonSerializer.SerializeToNode(System.Text.Json.JsonSerializer.Deserialize<dynamic>("{\n    \"a\": -0,\n    \"b\": 1e10\n}")).ToJsonString();
+        
+    }
+
+    public static String Serialize(object value, Type inputType, JsonSerializerOptions? options = null) => JsonSerializer.Serialize(value, inputType, _options);
+    // public static String Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => JsonSerializer.Serialize(value, jsonTypeInfo, _options);
+    // public static String Serialize(Object value, JsonTypeInfo jsonTypeInfo) 
+
+#endregion
+
+    private static partial class JsonExtensions {
+        public static Action<JsonTypeInfo> AlphabetizeProperties(Type type) {
+            return typeInfo => {
+                if (typeInfo.Kind != JsonTypeInfoKind.Object || !type.IsAssignableFrom(typeInfo.Type))
+                    return;
+                AlphabetizeProperties()(typeInfo);
+            };
+        }
+
+        public static Action<JsonTypeInfo> AlphabetizeProperties() {
+            return static typeInfo => {
+                if (typeInfo.Kind == JsonTypeInfoKind.Dictionary) { }
+
+                if (typeInfo.Kind != JsonTypeInfoKind.Object)
+                    return;
+                var properties = typeInfo.Properties.OrderBy(p => p.Name, StringComparer.Ordinal).ToList();
+                typeInfo.Properties.Clear();
+                for (int i = 0; i < properties.Count; i++) {
+                    properties[i].Order = i;
+                    typeInfo.Properties.Add(properties[i]);
+                }
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/LibMatrix/Extensions/UnicodeJsonEncoder.cs b/LibMatrix/Extensions/UnicodeJsonEncoder.cs
new file mode 100644
index 0000000..ae58263
--- /dev/null
+++ b/LibMatrix/Extensions/UnicodeJsonEncoder.cs
@@ -0,0 +1,173 @@
+// LibMatrix: File sourced from https://github.com/dotnet/runtime/pull/87147/files under the MIT license.
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using System.Text.Encodings.Web;
+
+namespace LibMatrix.Extensions;
+
+internal sealed class UnicodeJsonEncoder : JavaScriptEncoder
+{
+    internal static readonly UnicodeJsonEncoder Singleton = new UnicodeJsonEncoder();
+
+    private readonly bool _preferHexEscape;
+    private readonly bool _preferUppercase;
+
+    public UnicodeJsonEncoder()
+        : this(preferHexEscape: false, preferUppercase: false)
+    {
+    }
+
+    public UnicodeJsonEncoder(bool preferHexEscape, bool preferUppercase)
+    {
+        _preferHexEscape = preferHexEscape;
+        _preferUppercase = preferUppercase;
+    }
+
+    public override int MaxOutputCharactersPerInputCharacter => 6; // "\uXXXX" for a single char ("\uXXXX\uYYYY" [12 chars] for supplementary scalar value)
+
+    public override unsafe int FindFirstCharacterToEncode(char* text, int textLength)
+    {
+        for (int index = 0; index < textLength; ++index)
+        {
+            char value = text[index];
+
+            if (NeedsEncoding(value))
+            {
+                return index;
+            }
+        }
+
+        return -1;
+    }
+
+    public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten)
+    {
+        bool encode = WillEncode(unicodeScalar);
+
+        if (!encode)
+        {
+            Span<char> span = new Span<char>(buffer, bufferLength);
+            int spanWritten;
+            bool succeeded = new Rune(unicodeScalar).TryEncodeToUtf16(span, out spanWritten);
+            numberOfCharactersWritten = spanWritten;
+            return succeeded;
+        }
+
+        if (!_preferHexEscape && unicodeScalar <= char.MaxValue && HasTwoCharacterEscape((char)unicodeScalar))
+        {
+            if (bufferLength < 2)
+            {
+                numberOfCharactersWritten = 0;
+                return false;
+            }
+
+            buffer[0] = '\\';
+            buffer[1] = GetTwoCharacterEscapeSuffix((char)unicodeScalar);
+            numberOfCharactersWritten = 2;
+            return true;
+        }
+        else
+        {
+            if (bufferLength < 6)
+            {
+                numberOfCharactersWritten = 0;
+                return false;
+            }
+
+            buffer[0] = '\\';
+            buffer[1] = 'u';
+            buffer[2] = '0';
+            buffer[3] = '0';
+            buffer[4] = ToHexDigit((unicodeScalar & 0xf0) >> 4, _preferUppercase);
+            buffer[5] = ToHexDigit(unicodeScalar & 0xf, _preferUppercase);
+            numberOfCharactersWritten = 6;
+            return true;
+        }
+    }
+
+    public override bool WillEncode(int unicodeScalar)
+    {
+        if (unicodeScalar > char.MaxValue)
+        {
+            return false;
+        }
+
+        return NeedsEncoding((char)unicodeScalar);
+    }
+
+    // https://datatracker.ietf.org/doc/html/rfc8259#section-7
+    private static bool NeedsEncoding(char value)
+    {
+        if (value == '"' || value == '\\')
+        {
+            return true;
+        }
+
+        return value <= '\u001f';
+    }
+
+    private static bool HasTwoCharacterEscape(char value)
+    {
+        // RFC 8259, Section 7, "char = " BNF
+        switch (value)
+        {
+            case '"':
+            case '\\':
+            case '/':
+            case '\b':
+            case '\f':
+            case '\n':
+            case '\r':
+            case '\t':
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private static char GetTwoCharacterEscapeSuffix(char value)
+    {
+        // RFC 8259, Section 7, "char = " BNF
+        switch (value)
+        {
+            case '"':
+                return '"';
+            case '\\':
+                return '\\';
+            case '/':
+                return '/';
+            case '\b':
+                return 'b';
+            case '\f':
+                return 'f';
+            case '\n':
+                return 'n';
+            case '\r':
+                return 'r';
+            case '\t':
+                return 't';
+            default:
+                throw new ArgumentOutOfRangeException(nameof(value));
+        }
+    }
+
+    private static char ToHexDigit(int value, bool uppercase)
+    {
+        if (value > 0xf)
+        {
+            throw new ArgumentOutOfRangeException(nameof(value));
+        }
+
+        if (value < 10)
+        {
+            return (char)(value + '0');
+        }
+        else
+        {
+            return (char)(value - 0xa + (uppercase ? 'A' : 'a'));
+        }
+    }
+}
\ No newline at end of file
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index d0511ea..e037672 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -8,6 +8,7 @@
 
         <Optimize>true</Optimize>
         <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- Required for UnicodeJsonEncoder... -->
     </PropertyGroup>
 
     <ItemGroup>
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index fe2ee8d..b906f08 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -388,12 +388,12 @@ public class GenericRoom {
         await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban",
             new UserIdAndReason { UserId = userId, Reason = reason });
 
-    public async Task UnbanAsync(string userId) =>
+    public async Task UnbanAsync(string userId, string? reason = null) =>
         await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
-            new UserIdAndReason { UserId = userId });
+            new UserIdAndReason { UserId = userId, Reason = reason});
 
     public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) {
-        if (skipExisting && await GetStateAsync<RoomMemberEventContent>("m.room.member", userId) is not null)
+        if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null)
             return;
         await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason));
     }
diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs
index b40ccc6..4563ed3 100644
--- a/LibMatrix/RoomTypes/SpaceRoom.cs
+++ b/LibMatrix/RoomTypes/SpaceRoom.cs
@@ -4,6 +4,8 @@ using LibMatrix.Homeservers;
 namespace LibMatrix.RoomTypes;
 
 public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : GenericRoom(homeserver, roomId) {
+    public const string TypeName = "m.space";
+
     public async IAsyncEnumerable<GenericRoom> GetChildrenAsync(bool includeRemoved = false) {
         // var rooms = new List<GenericRoom>();
         var state = GetFullStateAsync();
@@ -31,7 +33,7 @@ public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId)
         });
         return resp;
     }
-    
+
     public async Task<EventIdResponse> AddChildByIdAsync(string id) {
         return await AddChildAsync(Homeserver.GetRoom(id));
     }