Skip to content

Commit

Permalink
Backend(,Tests): stop using binary serialization
Browse files Browse the repository at this point in the history
We had a need in the past to serialize exceptions (as they could
happen in off-line mode, when running as a cold-storage device),
so that they could be reported later when the device comes
online, but exceptions can't be seralizated to JSON (as
explained in [1]), so we ended up using binary serialization
(hooking it up in this past commit[2]).

However, binary serialization is going away in .NET9[3] because
of its potential security risk. Even though we doubt that for
our use case we would be affected by this security vector, we:

- Want to be prepared for the future.
- Know that there were anyway edge cases where binary
serialization was not actually working (e.g. see bug 240), and
was causing crashes.

We explored the idea of contributing an IException interface to
the 'sentry-dotnet' repo [4] (this library is the replacement of
SharpRaven, see [5]), so that we can serialize exceptions easily
in JSON, for later deserializing them and send them straight to
Sentry's API for report purposes, however:

* We found adding the IException overloads to be extremely
complicated due to the sheer amount of unit tests and things
that Sentry has, that would need to be modified.
* Given the above, we thought it would be too much work, and too
much risk of not being accepted upstream.
* Even if the IException overloads were accepted, the approach
would still be a leaky abstraction because the type of the
exception cannot be properly represented in a hypothetical
IException's property, so we were/would ending up with hacky
things such as an IsAggregateException:bool property, for
example. But why end here and not have more bool types for
other exceptions?

Instead of the above nightmare we have decided to go for the
simplest approach of all (the one that I should have done 3ish
years ago when I was initially solving this problem, to avoid
any OVERENGINEERING): just use good old Exception.ToString()
method! This method provides, not only the type of the
exception and its .Message property, also all its inner
exceptions recursively. This is GOOD ENOUGH.

Fixes #240
Closes https://gitlab.com/nblockchain/geewallet/-/issues/174

[1] 403d5c7
[2] 1f7b3b7
[3] https://twitter.com/SitnikAdam/status/1746874459640811575
[4] https://github.com/getsentry/sentry-dotnet
[5] #252
  • Loading branch information
knocte committed Jan 31, 2024
1 parent 9c7c1f4 commit 55c4cef
Show file tree
Hide file tree
Showing 15 changed files with 79 additions and 300 deletions.
49 changes: 9 additions & 40 deletions src/GWallet.Backend.Tests/ExceptionMarshalling.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ open NUnit.Framework
open GWallet.Backend


type CustomExceptionWithoutSerializationCtor =
type CustomExceptionWithoutInnerExceptionCtor =
inherit Exception

new(message) =
Expand All @@ -17,8 +17,6 @@ type CustomExceptionWithoutSerializationCtor =
type CustomException =
inherit Exception

new(info: SerializationInfo, context: StreamingContext) =
{ inherit Exception(info, context) }
new(message: string, innerException: CustomException) =
{ inherit Exception(message, innerException) }
new(message) =
Expand Down Expand Up @@ -78,7 +76,7 @@ type ExceptionMarshalling () =
let json = SerializeBasicException ()
Assert.That(json, Is.Not.Null)
Assert.That(json, Is.Not.Empty)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.BasicExceptionExampleInJson false msg)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.BasicExceptionExampleInJson msg)

[<Test>]
member __.``can deserialize basic exceptions``() =
Expand All @@ -102,20 +100,7 @@ type ExceptionMarshalling () =
let json = SerializeRealException ()
Assert.That(json, Is.Not.Null)
Assert.That(json, Is.Not.Empty)
#if !LEGACY_FRAMEWORK
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.RealExceptionExampleInJson false msg)
#else
if Config.IsWindowsPlatform () then
let serializedExceptionsAreSame =
try
MarshallingData.SerializedExceptionsAreSame json MarshallingData.RealExceptionExampleInJson false msg
with
| :? AssertionException ->
MarshallingData.SerializedExceptionsAreSame json MarshallingData.RealExceptionWindowsLegacyExampleInJson false legacyMsg
Assert.That serializedExceptionsAreSame
else
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.RealExceptionUnixLegacyExampleInJson false legacyMsg)
#endif
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.RealExceptionExampleInJson msg)

[<Test>]
member __.``can deserialize real exceptions``() =
Expand All @@ -140,7 +125,7 @@ type ExceptionMarshalling () =
let json = SerializeInnerException ()
Assert.That(json, Is.Not.Null)
Assert.That(json, Is.Not.Empty)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.InnerExceptionExampleInJson false msg)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.InnerExceptionExampleInJson msg)

[<Test>]
member __.``can deserialize inner exceptions``() =
Expand Down Expand Up @@ -168,15 +153,15 @@ type ExceptionMarshalling () =
let json = SerializeCustomException ()
Assert.That(json, Is.Not.Null)
Assert.That(json, Is.Not.Empty)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.CustomExceptionExampleInJson false msg)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.CustomExceptionExampleInJson msg)

[<Test>]
member __.``serializing custom exception not prepared for binary serialization, throws``() =
let exToSerialize = CustomExceptionWithoutSerializationCtor "msg"
let exToSerialize = CustomExceptionWithoutInnerExceptionCtor "msg"
let ex: MarshallingCompatibilityException =
Assert.Throws(fun _ -> Marshalling.Serialize exToSerialize |> ignore<string>)
Assert.That(ex, Is.TypeOf<MarshallingCompatibilityException>())
Assert.That(ex.Message, IsString.WhichContains "GWallet.Backend.Tests.CustomExceptionWithoutSerializationCtor")
Assert.That(ex.Message, IsString.WhichContains "GWallet.Backend.Tests.CustomExceptionWithoutInnerExceptionCtor")

[<Test>]
member __.``can deserialize custom exceptions``() =
Expand All @@ -200,9 +185,7 @@ type ExceptionMarshalling () =
let json = SerializeCustomFSharpException ()
Assert.That(json, Is.Not.Null)
Assert.That(json, Is.Not.Empty)

// strangely enough, message would be different between linux_vanilla_dotnet6 and other dotnet6 configs (e.g. Windows, macOS, Linux-github)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.CustomFSharpExceptionExampleInJson true msg)
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.CustomFSharpExceptionExampleInJson msg)

[<Test>]
member __.``can deserialize F# custom exceptions``() =
Expand Down Expand Up @@ -230,21 +213,7 @@ type ExceptionMarshalling () =

Assert.That(json, Is.Not.Null)
Assert.That(json, Is.Not.Empty)

#if !LEGACY_FRAMEWORK
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.FullExceptionExampleInJson false msg)
#else
if Config.IsWindowsPlatform () then
let serializedExceptionsAreSame =
try
MarshallingData.SerializedExceptionsAreSame json MarshallingData.FullExceptionExampleInJson false msg
with
| :? AssertionException ->
MarshallingData.SerializedExceptionsAreSame json MarshallingData.FullExceptionWindowsLegacyExampleInJson false legacyMsg
Assert.That serializedExceptionsAreSame
else
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.FullExceptionUnixLegacyExampleInJson false legacyMsg)
#endif
Assert.That(MarshallingData.SerializedExceptionsAreSame json MarshallingData.FullExceptionExampleInJson msg)

[<Test>]
member __.``can deserialize full exceptions (all previous features combined)``() =
Expand Down
5 changes: 0 additions & 5 deletions src/GWallet.Backend.Tests/GWallet.Backend.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,13 @@
<ItemGroup>
<EmbeddedResource Include="data\basicException.json" />
<EmbeddedResource Include="data\customFSharpException.json" />
<EmbeddedResource Include="data\customFSharpException_legacy.json" />
<EmbeddedResource Include="data\realException.json" />
<EmbeddedResource Include="data\realException_unixLegacy.json" />
<EmbeddedResource Include="data\realException_windowsLegacy.json" />
<EmbeddedResource Include="data\signedAndFormattedEtherTransaction.json" />
<EmbeddedResource Include="data\customException.json" />
<EmbeddedResource Include="data\unsignedAndFormattedSaiTransaction.json" />
<EmbeddedResource Include="data\unsignedAndFormattedBtcTransaction.json" />
<EmbeddedResource Include="data\unsignedAndFormattedEtherTransaction.json" />
<EmbeddedResource Include="data\fullException.json" />
<EmbeddedResource Include="data\fullException_unixLegacy.json" />
<EmbeddedResource Include="data\fullException_windowsLegacy.json" />
<EmbeddedResource Include="data\signedAndFormattedBtcTransaction.json" />
<EmbeddedResource Include="data\signedAndFormattedSaiTransaction.json" />
<EmbeddedResource Include="data\innerException.json" />
Expand Down
89 changes: 19 additions & 70 deletions src/GWallet.Backend.Tests/MarshallingData.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module MarshallingData =
let private executingAssembly = Assembly.GetExecutingAssembly()
let private version = VersionHelper.CURRENT_VERSION
let private binPath = executingAssembly.Location |> FileInfo
let private prjPath = Path.Combine(binPath.Directory.FullName, "..") |> DirectoryInfo
let private prjPath = Path.Combine(binPath.Directory.FullName, "..", "..", "..") |> DirectoryInfo

let private RemoveJsonFormatting (jsonContent: string): string =
jsonContent.Replace("\r", String.Empty)
Expand Down Expand Up @@ -50,12 +50,6 @@ module MarshallingData =
let RealExceptionExampleInJson =
ReadEmbeddedResource "realException.json"

let RealExceptionUnixLegacyExampleInJson =
ReadEmbeddedResource "realException_unixLegacy.json"

let RealExceptionWindowsLegacyExampleInJson =
ReadEmbeddedResource "realException_windowsLegacy.json"

let InnerExceptionExampleInJson =
ReadEmbeddedResource "innerException.json"

Expand All @@ -65,80 +59,35 @@ module MarshallingData =
let CustomFSharpExceptionExampleInJson =
ReadEmbeddedResource "customFSharpException.json"

let CustomFSharpExceptionLegacyExampleInJson =
ReadEmbeddedResource "customFSharpException_legacy.json"

let FullExceptionExampleInJson =
ReadEmbeddedResource "fullException.json"

let FullExceptionUnixLegacyExampleInJson =
ReadEmbeddedResource "fullException_unixLegacy.json"

let FullExceptionWindowsLegacyExampleInJson =
ReadEmbeddedResource "fullException_windowsLegacy.json"

let rec TrimOutsideAndInside(str: string) =
let trimmed = str.Replace(" ", " ").Trim()
if trimmed = str then
trimmed
else
TrimOutsideAndInside trimmed

let SerializedExceptionsAreSame actualJsonString expectedJsonString ignoreExMessage msg =

let actualJsonException = JObject.Parse actualJsonString
let expectedJsonException = JObject.Parse expectedJsonString

let fullBinaryFormPath = "Value.FullBinaryForm"
let tweakStackTraces () =

let fullBinaryFormBeginning = "AAEAAAD/////AQAA"
let stackTracePath = "Value.HumanReadableSummary.StackTrace"
let stackTraceFragment = "ExceptionMarshalling.fs"

let tweakStackTraceAndBinaryForm (jsonEx: JObject) (assertBinaryForm: bool) =
let stackTraceJToken = jsonEx.SelectToken stackTracePath
Assert.That(stackTraceJToken, Is.Not.Null, sprintf "Path %s not found in %s" stackTracePath (jsonEx.ToString()))
let initialStackTraceJToken = stackTraceJToken.ToString()
if initialStackTraceJToken.Length > 0 then
Assert.That(initialStackTraceJToken, IsString.WhichContains stackTraceFragment,
sprintf "comparing actual '%s' with expected '%s'" actualJsonString expectedJsonString)
let endOfStackTrace = initialStackTraceJToken.Substring(initialStackTraceJToken.IndexOf stackTraceFragment)
let tweakedEndOfStackTrace =
endOfStackTrace
.Replace(":line 42", ":41 ")
.Replace(":line 41", ":41 ")
.Replace(":line 65", ":64 ")
.Replace(":line 64", ":64 ")
stackTraceJToken.Replace (tweakedEndOfStackTrace |> JToken.op_Implicit)

let binaryFormToken = jsonEx.SelectToken fullBinaryFormPath
Assert.That(binaryFormToken, Is.Not.Null, sprintf "Path %s not found in %s" fullBinaryFormPath (jsonEx.ToString()))
let initialBinaryFormJToken = binaryFormToken.ToString()
if assertBinaryForm then
Assert.That(initialBinaryFormJToken, IsString.StartingWith fullBinaryFormBeginning)
binaryFormToken.Replace (fullBinaryFormBeginning |> JToken.op_Implicit)

tweakStackTraceAndBinaryForm actualJsonException true
tweakStackTraceAndBinaryForm expectedJsonException false

tweakStackTraces()

// strangely enough, message would be different between linux_vanilla_dotnet6 and other dotnet6 configs (e.g. Windows, macOS, Linux-github)
if ignoreExMessage then
let exMessagePath = "Value.HumanReadableSummary.Message"
let actualMsgToken = actualJsonException.SelectToken exMessagePath
Assert.That(actualMsgToken, Is.Not.Null, sprintf "Path %s not found in %s" exMessagePath (actualJsonException.ToString()))
let expectedMsgToken = expectedJsonException.SelectToken exMessagePath
Assert.That(expectedMsgToken, Is.Not.Null, sprintf "Path %s not found in %s" exMessagePath (expectedJsonException.ToString()))
actualMsgToken.Replace(String.Empty |> JToken.op_Implicit)
expectedMsgToken.Replace(String.Empty |> JToken.op_Implicit)

let actualBinaryForm = (actualJsonException.SelectToken fullBinaryFormPath).ToString()
let SerializedExceptionsAreSame actualJsonString expectedJsonString (msg: string) =
let actualJson = JObject.Parse actualJsonString
Assert.That(actualJson, Is.Not.Null)
let expectedJson = JObject.Parse expectedJsonString
Assert.That(expectedJson, Is.Not.Null)

let fullDescPath = "Value.FullDescription"
let actualJsonToken = actualJson.SelectToken fullDescPath
Assert.That(actualJsonToken, Is.Not.Null)
let expectedJsonToken = expectedJson.SelectToken fullDescPath
Assert.That(expectedJsonToken, Is.Not.Null)

let actualFullDesc = actualJsonToken.ToString()
let expectedFullDesc = expectedJsonToken.ToString()

Assert.That(
TrimOutsideAndInside(actualJsonException.ToString()),
Is.EqualTo (TrimOutsideAndInside(expectedJsonException.ToString())),
msg + actualBinaryForm
TrimOutsideAndInside actualFullDesc,
Is.EqualTo (TrimOutsideAndInside expectedFullDesc),
msg
)

true
Expand Down
9 changes: 2 additions & 7 deletions src/GWallet.Backend.Tests/data/basicException.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
"Version": "{version}",
"TypeName": "GWallet.Backend.MarshalledException",
"Value": {
"HumanReadableSummary": {
"ExceptionType": "System.Exception",
"Message": "msg",
"StackTrace": "",
"InnerException": null
},
"FullBinaryForm": "{binaryMarshalledException}"
"DateTimeUtc": "<this value is not compared in tests>",
"FullDescription": "System.Exception: msg"
}
}
9 changes: 2 additions & 7 deletions src/GWallet.Backend.Tests/data/customException.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
"Version": "{version}",
"TypeName": "GWallet.Backend.MarshalledException",
"Value": {
"HumanReadableSummary": {
"ExceptionType": "GWallet.Backend.Tests.CustomException",
"Message": "msg",
"StackTrace": "",
"InnerException": null
},
"FullBinaryForm": "{binaryMarshalledException}"
"DateTimeUtc": "<this value is not compared in tests>",
"FullDescription": "GWallet.Backend.Tests.CustomException: msg"
}
}
9 changes: 2 additions & 7 deletions src/GWallet.Backend.Tests/data/customFSharpException.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
"Version": "{version}",
"TypeName": "GWallet.Backend.MarshalledException",
"Value": {
"HumanReadableSummary": {
"ExceptionType": "GWallet.Backend.Tests.CustomFSharpException",
"Message": "Exception of type 'GWallet.Backend.Tests.CustomFSharpException' was thrown.",
"StackTrace": "",
"InnerException": null
},
"FullBinaryForm": "{binaryMarshalledException}"
"DateTimeUtc": "<this value is not compared in tests>",
"FullDescription": "GWallet.Backend.Tests.CustomFSharpException: CustomFSharpException"
}
}
13 changes: 0 additions & 13 deletions src/GWallet.Backend.Tests/data/customFSharpException_legacy.json

This file was deleted.

23 changes: 0 additions & 23 deletions src/GWallet.Backend.Tests/data/fullException_unixLegacy.json

This file was deleted.

23 changes: 0 additions & 23 deletions src/GWallet.Backend.Tests/data/fullException_windowsLegacy.json

This file was deleted.

19 changes: 2 additions & 17 deletions src/GWallet.Backend.Tests/data/innerException.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,7 @@
"Version": "{version}",
"TypeName": "GWallet.Backend.MarshalledException",
"Value": {
"HumanReadableSummary": {
"ExceptionType": "System.Exception",
"Message": "msg",
"StackTrace": "",
"InnerException": {
"Case": "Some",
"Fields": [
{
"ExceptionType": "System.Exception",
"Message": "innerMsg",
"StackTrace": "",
"InnerException": null
}
]
}
},
"FullBinaryForm": "{binaryMarshalledException}"
"DateTimeUtc": "<this value is not compared in tests>",
"FullDescription": "GWallet.Backend.Tests.CustomFSharpException: CustomFSharpException"
}
}
Loading

0 comments on commit 55c4cef

Please sign in to comment.