From 1afdfc89af3cf3cb148b0e6fd3a99477487dfd23 Mon Sep 17 00:00:00 2001 From: "Andres G. Aragoneses" Date: Wed, 16 Dec 2020 14:53:34 +0800 Subject: [PATCH] Backend(,Tests): implement basic exception deserialization JSON deserialization of exceptions is extremely broken in JSON.NET [1] (and even in System.Text.Json [2]), so let's use a hybrid between binary serialization and JSON serialization: it will have a JSON part that hints about the most important human-readable details, but it will use a JSON element to marshall in binary-to-default-encoding-based-string format. (I could have written a JsonConverter for this, but I was having trouble finding one out there that someone else had written already, then I decided to go for the simplest solution that I can write myself. I hate JSON type converters anyway.) [1] https://github.com/JamesNK/Newtonsoft.Json/issues/801 [2] https://github.com/dotnet/runtime/issues/43026 [3] https://stackoverflow.com/a/13674508/544947 --- .../ExceptionMarshalling.fs | 14 +++++ src/GWallet.Backend.Tests/MarshallingData.fs | 55 +++++++----------- .../data/basicException.json | 21 +++---- src/GWallet.Backend/BinaryMarshalling.fs | 28 +++++++++ src/GWallet.Backend/GWallet.Backend.fsproj | 1 + src/GWallet.Backend/Marshalling.fs | 58 ++++++++++++++++++- 6 files changed, 127 insertions(+), 50 deletions(-) create mode 100644 src/GWallet.Backend/BinaryMarshalling.fs diff --git a/src/GWallet.Backend.Tests/ExceptionMarshalling.fs b/src/GWallet.Backend.Tests/ExceptionMarshalling.fs index d0041f0e5..21485ec7a 100644 --- a/src/GWallet.Backend.Tests/ExceptionMarshalling.fs +++ b/src/GWallet.Backend.Tests/ExceptionMarshalling.fs @@ -27,6 +27,16 @@ type ExceptionMarshalling () = Is.EqualTo MarshallingData.BasicExceptionExampleInJson) [] + member __.``can deserialize basic exceptions``() = + let ex: Exception = Marshalling.Deserialize MarshallingData.BasicExceptionExampleInJson + Assert.That(ex, Is.Not.Null) + Assert.That(ex, Is.InstanceOf()) + Assert.That(ex.Message, Is.EqualTo "msg") + Assert.That(ex.InnerException, Is.Null) + Assert.That(ex.StackTrace, Is.Null) + + [] + [] member __.``can serialize real exceptions``() = let someEx = Exception "msg" let ex = @@ -42,6 +52,7 @@ type ExceptionMarshalling () = Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.RealExceptionExampleInJson) [] + [] member __.``can serialize inner exceptions``() = let ex = Exception("msg", Exception "innerMsg") let json = Marshalling.Serialize ex @@ -51,6 +62,7 @@ type ExceptionMarshalling () = Is.EqualTo MarshallingData.InnerExceptionExampleInJson) [] + [] member __.``can serialize custom exceptions``() = let ex = CustomException "msg" let json = Marshalling.Serialize ex @@ -60,6 +72,7 @@ type ExceptionMarshalling () = Is.EqualTo MarshallingData.CustomExceptionExampleInJson) [] + [] member __.``can serialize full exceptions (all previous features combined)``() = let someCEx = CustomException("msg", CustomException "innerMsg") let ex = @@ -74,3 +87,4 @@ type ExceptionMarshalling () = Assert.That(json, Is.Not.Empty) Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.FullExceptionExampleInJson) + diff --git a/src/GWallet.Backend.Tests/MarshallingData.fs b/src/GWallet.Backend.Tests/MarshallingData.fs index 47b228f9c..674b26d81 100644 --- a/src/GWallet.Backend.Tests/MarshallingData.fs +++ b/src/GWallet.Backend.Tests/MarshallingData.fs @@ -29,43 +29,31 @@ module MarshallingData = let private InjectCurrentDir (jsonContent: string): string = jsonContent.Replace("{prjDirAbsolutePath}", prjPath.FullName.Replace("\\", "/")) - let private NormalizeExceptions (jsonContent: string): string = - if System.IO.Path.DirectorySeparatorChar <> '\\' then - let weirdWindowsProperty = "\"WatsonBuckets\": null" - Assert.That(jsonContent, Is.StringContaining weirdWindowsProperty) - let removeWindowsProperty = jsonContent.Replace(", " + weirdWindowsProperty, String.Empty) - .Replace(", " + weirdWindowsProperty, String.Empty) - - removeWindowsProperty.Replace(":line 35", ":34 ").Replace(":line 68", ":67 ") - .Replace("\"8\\ncan serialize real exceptions\\nGWallet.Backend.Tests, Version={version}, Culture=neutral, PublicKeyToken=null\\nGWallet.Backend.Tests.ExceptionMarshalling\\nVoid can serialize real exceptions()\"", - "null") - .Replace("\"8\\ncan serialize full exceptions (all previous features combined)\\nGWallet.Backend.Tests, Version={version}, Culture=neutral, PublicKeyToken=null\\nGWallet.Backend.Tests.ExceptionMarshalling\\nVoid can serialize full exceptions (all previous features combined)()\"", - "null") - else - jsonContent + let private InjectExceptionsMarshalledInBinary (isUnix: bool) (jsonContent: string): string = + let marshalledEx = + if isUnix then + "AAEAAAD/////AQAAAAAAAAAEAQAAABBTeXN0ZW0uRXhjZXB0aW9uCwAAAAlDbGFzc05hbWUHTWVzc2FnZQREYXRhDklubmVyRXhjZXB0aW9uB0hlbHBVUkwQU3RhY2tUcmFjZVN0cmluZxZSZW1vdGVTdGFja1RyYWNlU3RyaW5nEFJlbW90ZVN0YWNrSW5kZXgPRXhjZXB0aW9uTWV0aG9kB0hSZXN1bHQGU291cmNlAQEDAwEBAQACAAEeU3lzdGVtLkNvbGxlY3Rpb25zLklEaWN0aW9uYXJ5EFN5c3RlbS5FeGNlcHRpb24ICAYCAAAAEFN5c3RlbS5FeGNlcHRpb24GAwAAAANtc2cKCgoKCgAAAAAKABUTgAoL" + else + "AAEAAAD/////AQAAAAAAAAAEAQAAABBTeXN0ZW0uRXhjZXB0aW9uDAAAAAlDbGFzc05hbWUHTWVzc2FnZQREYXRhDklubmVyRXhjZXB0aW9uB0hlbHBVUkwQU3RhY2tUcmFjZVN0cmluZxZSZW1vdGVTdGFja1RyYWNlU3RyaW5nEFJlbW90ZVN0YWNrSW5kZXgPRXhjZXB0aW9uTWV0aG9kB0hSZXN1bHQGU291cmNlDVdhdHNvbkJ1Y2tldHMBAQMDAQEBAAEAAQceU3lzdGVtLkNvbGxlY3Rpb25zLklEaWN0aW9uYXJ5EFN5c3RlbS5FeGNlcHRpb24ICAIGAgAAABBTeXN0ZW0uRXhjZXB0aW9uBgMAAAADbXNnCgoKCgoAAAAACgAVE4AKCgs=" + + jsonContent.Replace("{binaryMarshalledException}", marshalledEx) let internal Sanitize = - RemoveJsonFormatting >> InjectCurrentVersion >> InjectCurrentDir + let isUnix = Path.DirectorySeparatorChar <> '\\' - let internal SanitizeExceptions = - RemoveJsonFormatting >> NormalizeExceptions >> InjectCurrentVersion >> InjectCurrentDir + RemoveJsonFormatting + >> InjectCurrentVersion + >> InjectCurrentDir + >> (InjectExceptionsMarshalledInBinary isUnix) - let private ReadEmbeddedResourceInternal resourceName exceptions = - let sanitizeFunc = - if exceptions then - SanitizeExceptions - else - Sanitize + let private ReadEmbeddedResource resourceName = let assembly = Assembly.GetExecutingAssembly() use stream = assembly.GetManifestResourceStream resourceName if (stream = null) then failwithf "Embedded resource %s not found" resourceName use reader = new StreamReader(stream) reader.ReadToEnd() - |> sanitizeFunc - - let private ReadEmbeddedResource resourceName = - ReadEmbeddedResourceInternal resourceName false + |> Sanitize let UnsignedSaiTransactionExampleInJson = ReadEmbeddedResource "unsignedAndFormattedSaiTransaction.json" @@ -73,23 +61,20 @@ module MarshallingData = let SignedSaiTransactionExampleInJson = ReadEmbeddedResource "signedAndFormattedSaiTransaction.json" - let private ReadException fileName = - ReadEmbeddedResourceInternal fileName true - let BasicExceptionExampleInJson = - ReadException "basicException.json" + ReadEmbeddedResource "basicException.json" let RealExceptionExampleInJson = - ReadException "realException.json" + ReadEmbeddedResource "realException.json" let InnerExceptionExampleInJson = - ReadException "innerException.json" + ReadEmbeddedResource "innerException.json" let CustomExceptionExampleInJson = - ReadException "customException.json" + ReadEmbeddedResource "customException.json" let FullExceptionExampleInJson = - ReadException "fullException.json" + ReadEmbeddedResource "fullException.json" let SerializedExceptionsAreSame jsonExString1 jsonExString2 = let stackTracePath = "Value.StackTraceString" diff --git a/src/GWallet.Backend.Tests/data/basicException.json b/src/GWallet.Backend.Tests/data/basicException.json index 2085bbe5f..3bcfd9ce3 100644 --- a/src/GWallet.Backend.Tests/data/basicException.json +++ b/src/GWallet.Backend.Tests/data/basicException.json @@ -1,18 +1,13 @@ { "Version": "{version}", - "TypeName": "System.Exception", + "TypeName": "GWallet.Backend.MarshalledException", "Value": { - "ClassName": "System.Exception", - "Message": "msg", - "Data": null, - "InnerException": null, - "HelpURL": null, - "StackTraceString": null, - "RemoteStackTraceString": null, - "RemoteStackIndex": 0, - "ExceptionMethod": null, - "HResult": -2146233088, - "Source": null, - "WatsonBuckets": null + "HumanReadableSummary": { + "ExceptionType": "System.Exception", + "Message": "msg", + "StackTrace": "", + "InnerException": null + }, + "FullBinaryForm": "{binaryMarshalledException}" } } \ No newline at end of file diff --git a/src/GWallet.Backend/BinaryMarshalling.fs b/src/GWallet.Backend/BinaryMarshalling.fs new file mode 100644 index 000000000..9a8e20939 --- /dev/null +++ b/src/GWallet.Backend/BinaryMarshalling.fs @@ -0,0 +1,28 @@ +namespace GWallet.Backend + +open System +open System.IO +open System.Text +open System.Runtime.Serialization.Formatters.Binary + +module BinaryMarshalling = + + let private binFormatter = BinaryFormatter() + + let Serialize obj: array = + use memStream = new MemoryStream() + binFormatter.Serialize(memStream, obj) + memStream.ToArray() + + let Deserialize (buffer: array) = + use memStream = new MemoryStream(buffer) + memStream.Position <- 0L + binFormatter.Deserialize memStream + + let SerializeToString obj: string = + let byteArray = Serialize obj + Convert.ToBase64String byteArray + + let DeserializeFromString (byteArrayString: string) = + let byteArray = Convert.FromBase64String byteArrayString + Deserialize byteArray diff --git a/src/GWallet.Backend/GWallet.Backend.fsproj b/src/GWallet.Backend/GWallet.Backend.fsproj index 3ce40b536..045b8a555 100644 --- a/src/GWallet.Backend/GWallet.Backend.fsproj +++ b/src/GWallet.Backend/GWallet.Backend.fsproj @@ -54,6 +54,7 @@ + diff --git a/src/GWallet.Backend/Marshalling.fs b/src/GWallet.Backend/Marshalling.fs index 57b9ab6ba..6a7c07ef0 100644 --- a/src/GWallet.Backend/Marshalling.fs +++ b/src/GWallet.Backend/Marshalling.fs @@ -9,6 +9,50 @@ open Newtonsoft.Json.Serialization open GWallet.Backend.FSharpUtil.UwpHacks +type ExceptionDetails = + { + ExceptionType: string + Message: string + StackTrace: string + InnerException: Option + } + +type MarshalledException = + { + HumanReadableSummary: ExceptionDetails + FullBinaryForm: string + } + static member private ExtractBasicDetailsFromException (ex: Exception) = + let stackTrace = + if ex.StackTrace = null then + String.Empty + else + ex.StackTrace + let stub = + { + ExceptionType = ex.GetType().FullName + Message = ex.Message + StackTrace = stackTrace + InnerException = None + } + + match ex.InnerException with + | null -> stub + | someNonNullInnerException -> + let innerExceptionDetails = + MarshalledException.ExtractBasicDetailsFromException someNonNullInnerException + + { + stub with + InnerException = Some innerExceptionDetails + } + + static member Create (ex: Exception) = + { + HumanReadableSummary = MarshalledException.ExtractBasicDetailsFromException ex + FullBinaryForm = BinaryMarshalling.SerializeToString ex + } + type DeserializationException = inherit Exception @@ -124,7 +168,12 @@ module Marshalling = deserialized.Value let Deserialize<'T>(json: string): 'T = - DeserializeCustom(json, DefaultSettings) + match typeof<'T> with + | t when t = typeof -> + let marshalledException: MarshalledException = DeserializeCustom(json, DefaultSettings) + BinaryMarshalling.DeserializeFromString marshalledException.FullBinaryForm :?> 'T + | _ -> + DeserializeCustom(json, DefaultSettings) let private SerializeInternal<'T>(value: 'T) (settings: JsonSerializerSettings): string = JsonConvert.SerializeObject(MarshallingWrapper<'T>.New value, @@ -140,4 +189,9 @@ module Marshalling = (typeof<'T>.FullName) value, exn)) let Serialize<'T>(value: 'T): string = - SerializeCustom(value, DefaultSettings) + match box value with + | :? Exception as ex -> + let serializedEx = MarshalledException.Create ex + SerializeCustom(serializedEx, DefaultSettings) + | _ -> + SerializeCustom(value, DefaultSettings)