diff --git a/Cargo.lock b/Cargo.lock
index dce94446..0a09f10b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -475,11 +475,10 @@ dependencies = [
[[package]]
name = "diplomat"
-version = "0.5.2"
-source = "git+https://github.com/CBenoit/diplomat.git?rev=f4d93d57e93025c57e75e9711d2418faa9311a42#f4d93d57e93025c57e75e9711d2418faa9311a42"
+version = "0.15.0"
+source = "git+https://github.com/irvingoujAtDevolution/diplomat.git?rev=68bccfd8d61e146673ccdb2b24bd9f89dc25431c#68bccfd8d61e146673ccdb2b24bd9f89dc25431c"
dependencies = [
"diplomat_core",
- "lazy_static",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -487,20 +486,19 @@ dependencies = [
[[package]]
name = "diplomat-runtime"
-version = "0.5.2"
-source = "git+https://github.com/CBenoit/diplomat.git?rev=f4d93d57e93025c57e75e9711d2418faa9311a42#f4d93d57e93025c57e75e9711d2418faa9311a42"
+version = "0.15.1"
+source = "git+https://github.com/irvingoujAtDevolution/diplomat.git?rev=68bccfd8d61e146673ccdb2b24bd9f89dc25431c#68bccfd8d61e146673ccdb2b24bd9f89dc25431c"
[[package]]
name = "diplomat_core"
-version = "0.5.2"
-source = "git+https://github.com/CBenoit/diplomat.git?rev=f4d93d57e93025c57e75e9711d2418faa9311a42#f4d93d57e93025c57e75e9711d2418faa9311a42"
+version = "0.15.0"
+source = "git+https://github.com/irvingoujAtDevolution/diplomat.git?rev=68bccfd8d61e146673ccdb2b24bd9f89dc25431c#68bccfd8d61e146673ccdb2b24bd9f89dc25431c"
dependencies = [
- "lazy_static",
"proc-macro2",
"quote",
"serde",
"smallvec",
- "strck_ident",
+ "strck",
"syn 2.0.117",
]
@@ -1139,12 +1137,6 @@ dependencies = [
"cpufeatures 0.3.0",
]
-[[package]]
-name = "lazy_static"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -2119,17 +2111,10 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "strck"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be91090ded9d8f979d9fe921777342d37e769e0b6b7296843a7a38247240e917"
-
-[[package]]
-name = "strck_ident"
-version = "0.1.2"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1c3802b169b3858a44667f221c9a0b3136e6019936ea926fc97fbad8af77202"
+checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f"
dependencies = [
- "strck",
"unicode-ident",
]
diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml
index 7d31c4b6..cf5edff3 100644
--- a/ffi/Cargo.toml
+++ b/ffi/Cargo.toml
@@ -25,8 +25,8 @@ rand = "0.10"
rand_core = "0.10"
# FFI
-diplomat = { git = "https://github.com/CBenoit/diplomat.git", rev = "f4d93d57e93025c57e75e9711d2418faa9311a42" }
-diplomat-runtime = { git = "https://github.com/CBenoit/diplomat.git", rev = "f4d93d57e93025c57e75e9711d2418faa9311a42" }
+diplomat = { git = "https://github.com/irvingoujAtDevolution/diplomat.git", rev = "68bccfd8d61e146673ccdb2b24bd9f89dc25431c" }
+diplomat-runtime = { git = "https://github.com/irvingoujAtDevolution/diplomat.git", rev = "68bccfd8d61e146673ccdb2b24bd9f89dc25431c" }
time = "0.3"
hex = "0.4"
diff --git a/ffi/dotnet-interop-conf.toml b/ffi/dotnet-interop-conf.toml
index 4c8f9e37..ab717ef0 100644
--- a/ffi/dotnet-interop-conf.toml
+++ b/ffi/dotnet-interop-conf.toml
@@ -1,10 +1,7 @@
+[dotnet]
namespace = "Devolutions.Picky"
-native_lib = "DevolutionsPicky"
-
-[exceptions]
-trim_suffix = "Error"
-error_message_method = "ToDisplay"
-
-[properties]
-setters_prefix = "set_"
-getters_prefix = "get_"
+native-lib = "DevolutionsPicky"
+exception-trim-suffix = "Error"
+exception-message-method = "ToDisplay"
+getters-prefix = "get_"
+setters-prefix = "set_"
diff --git a/ffi/dotnet/Devolutions.Picky.Net48Tests/Devolutions.Picky.Net48Tests.csproj b/ffi/dotnet/Devolutions.Picky.Net48Tests/Devolutions.Picky.Net48Tests.csproj
new file mode 100644
index 00000000..89e9f2da
--- /dev/null
+++ b/ffi/dotnet/Devolutions.Picky.Net48Tests/Devolutions.Picky.Net48Tests.csproj
@@ -0,0 +1,54 @@
+
+
+
+
+
+ net48;net10.0
+ latest
+ enable
+ false
+ true
+ x64
+ x64
+ Devolutions.Picky.Net48Tests
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+ Generated\%(Filename)%(Extension)
+
+
+ Addons\%(Filename)%(Extension)
+
+
+
+
+
+
+ DevolutionsPicky.dll
+ PreserveNewest
+
+
+
+
diff --git a/ffi/dotnet/Devolutions.Picky.Net48Tests/FfiMarshallingTests.cs b/ffi/dotnet/Devolutions.Picky.Net48Tests/FfiMarshallingTests.cs
new file mode 100644
index 00000000..9bcf169e
--- /dev/null
+++ b/ffi/dotnet/Devolutions.Picky.Net48Tests/FfiMarshallingTests.cs
@@ -0,0 +1,217 @@
+using System;
+
+using Xunit;
+
+namespace Devolutions.Picky.Net48Tests;
+
+// These tests deliberately run on .NET Framework 4.8. The point isn't to
+// re-test picky's crypto — it's to prove that every *shape* of value crossing
+// the FFI boundary marshals correctly under the legacy CLR, which is where the
+// old by-value Result struct used to break. Each test is labelled with the
+// marshalling shape it exercises.
+public class FfiMarshallingTests
+{
+ // A real RSA key so we can exercise the RSA-only paths without paying for key generation.
+ private const string RsaPrivateKeyPem = @"-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDkrPiL/5dmGIT5
+/KuC3H/jIjeLoLoddsLhAlikO5JQQo3Zs71GwT4Wd2z8WLMe0lVZu/Jr2S28p0M8
+F3Lnz4IgzjocQomFgucFWWQRyD03ZE2BHfEeelFsp+/4GZaM6lKZauYlIMtjR1vD
+lflgvxNTr0iaii4JR9K3IKCunCRy1HQYPcZ9waNtlG5xXtW9Uf1tLWPJpP/3I5HL
+M85JPBv4r286vpeUlfQIa/NB4g5w6KZ6MfEAIU4KeEQpeLAyyYvwUzPR2uQZ4y4I
+4Nj84dWYB1cMTlSGugvSgOFKYit1nwLGeA7EevVYPbILRfSMBU/+avGNJJ8HCaaq
+FIyY42W9AgMBAAECggEBAImsGXcvydaNrIFUvW1rkxML5qUJfwN+HJWa9ALsWoo3
+h28p5ypR7S9ZdyP1wuErgHcl0C1d80tA6BmlhGhLZeyaPCIHbQQUa0GtL7IE+9X9
+bSvu+tt+iMcB1FdqEFmGOXRkB2sS82Ax9e0qvZihcOFRBkUEK/MqapIV8qctGkSG
+wIE6yn5LHRls/fJU8BJeeqJmYpuWljipwTkp9hQ7SdRYFLNjwjlz/b0hjmgFs5QZ
+LUNMyTHdHtXQHNsf/GayRUAKf5wzN/jru+nK6lMob2Ehfx9/RAfgaDHzy5BNFMj0
+i9+sAycgIW1HpTuDvSEs3qP26NeQ82GbJzATmdAKa4ECgYEA9Vti0YG+eXJI3vdS
+uXInU0i1SY4aEG397OlGMwh0yQnp2KGruLZGkTvqxG/Adj1ObDyjFH9XUhMrd0za
+Nk/VJFybWafljUPcrfyPAVLQLjsBfMg3Y34sTF6QjUnhg49X2jfvy9QpC5altCtA
+46/KVAGREnQJ3wMjfGGIFP8BUZsCgYEA7phYE/cYyWg7a/o8eKOFGqs11ojSqG3y
+0OE7kvW2ugUuy3ex+kr19Q/8pOWEc7M1UEV8gmc11xgB70EhIFt9Jq379H0X4ahS
++mgLiPzKAdNCRPpkxwwN9HxFDgGWoYcgMplhoAmg9lWSDuE1Exy8iu5inMWuF4MT
+/jG+cLnUZ4cCgYAfMIXIUjDvaUrAJTp73noHSUfaWNkRW5oa4rCMzjdiUwNKCYs1
+yN4BmldGr1oM7dApTDAC7AkiotM0sC1RGCblH2yUIha5NXY5G9Dl/yv9pHyU6zK3
+UBO7hY3kmA611aP6VoACLi8ljPn1hEYUa4VR1n0llmCm29RH/HH7EUuOnwKBgExH
+OCFp5eq+AAFNRvfqjysvgU7M/0wJmo9c8obRN1HRRlyWL7gtLuTh74toNSgoKus2
+y8+E35mce0HaOJT3qtMq3FoVhAUIoz6a9NUevBZJS+5xfraEDBIViJ4ps9aANLL4
+hlV7vpICWWeYaDdsAHsKK0yjhjzOEx45GQFA578RAoGBAOB42BG53tL0G9pPeJPt
+S2LM6vQKeYx+gXTk6F335UTiiC8t0CgNNQUkW105P/SdpCTTKojAsOPMKOF7z4mL
+lj/bWmNq7xu9uVOcBKrboVFGO/n6FXyWZxHPOTdjTkpe8kvvmSwl2iaTNllvSr46
+Z/fDKMxHxeXla54kfV+HiGkH
+-----END PRIVATE KEY-----";
+
+ // Result, E> returning an opaque object — the shape that used to
+ // break: a by-value struct carrying a pointer union + discriminant.
+ [Fact]
+ public void ResultReturningObject_Ec()
+ {
+ using PrivateKey key = PrivateKey.GenerateEc(EcCurve.NistP256);
+ Assert.Equal(KeyKind.Ec, key.Kind);
+ Assert.Equal(KeyKind.Ec, key.GetKind());
+ }
+
+ [Fact]
+ public void ResultReturningObject_Ed()
+ {
+ using PrivateKey key = PrivateKey.GenerateEd(EdAlgorithm.Ed25519, false);
+ Assert.Equal(KeyKind.Ed, key.Kind);
+ }
+
+ [Fact]
+ public void ResultReturningObject_FromPem_Rsa()
+ {
+ using Pem pem = Pem.Parse(RsaPrivateKeyPem);
+ using PrivateKey key = PrivateKey.FromPem(pem);
+ Assert.Equal(KeyKind.Rsa, key.Kind);
+
+ using PublicKey pub = key.ToPublicKey();
+ Assert.Equal(KeyKind.Rsa, pub.Kind);
+ }
+
+ // Result<(), E> — the void-success arm.
+ [Fact]
+ public void ResultReturningVoid_SignatureVerifyFailsCleanly()
+ {
+ using Pem pem = Pem.Parse(RsaPrivateKeyPem);
+ using PrivateKey priv = PrivateKey.FromPem(pem);
+ using PublicKey pub = priv.ToPublicKey();
+ using SignatureAlgorithm alg = SignatureAlgorithm.NewRsaPkcs1v15(HashAlgorithm.Sha2256);
+
+ // A bogus signature must surface as a PickyException, not a crash.
+ Assert.Throws(() =>
+ alg.Verify(pub, new byte[] { 1, 2, 3 }, new byte[] { 9, 9, 9 }));
+ }
+
+ // Result — a scalar success arm (long).
+ [Fact]
+ public void ResultReturningPrimitive_UtcDateTimestamp()
+ {
+ using UtcDate date = UtcDate.FromTimestamp(1_600_000_000);
+ Assert.Equal(1_600_000_000, date.Timestamp);
+ Assert.Equal(1_600_000_000, date.GetTimestamp());
+ }
+
+ // Result via a DiplomatWrite out-param — the string-return shape.
+ [Fact]
+ public void ResultReturningString_PemRoundTrip()
+ {
+ byte[] payload = { 0xDE, 0xAD, 0xBE, 0xEF };
+ using Pem pem = Pem.New("TEST DATA", payload);
+
+ string repr = pem.ToRepr();
+ Assert.Contains("TEST DATA", repr);
+
+ Assert.Equal("TEST DATA", pem.Label);
+ Assert.Equal("TEST DATA", pem.GetLabel());
+ }
+
+ // Hand-written addon returning byte[] (Pem.ToData / PublicKey.ToPkcs1).
+ [Fact]
+ public void AddonReturningByteArray_RoundTrip()
+ {
+ byte[] payload = { 1, 2, 3, 4, 5 };
+ using Pem pem = Pem.New("BLOB", payload);
+ Assert.Equal(payload, pem.ToData());
+ Assert.Equal((ulong)payload.Length, pem.DataLength);
+ }
+
+ [Fact]
+ public void AddonReturningByteArray_Pkcs1()
+ {
+ using Pem pem = Pem.Parse(RsaPrivateKeyPem);
+ using PrivateKey priv = PrivateKey.FromPem(pem);
+ using PublicKey pub = priv.ToPublicKey();
+ byte[] pkcs1 = pub.ToPkcs1();
+ Assert.NotEmpty(pkcs1);
+ }
+
+ // Option> — nullable opaque return.
+ [Fact]
+ public void OptionReturningObject_UtcDateYmd()
+ {
+ UtcDate? valid = UtcDate.Ymd(2020, 1, 1);
+ Assert.NotNull(valid);
+ using (valid)
+ {
+ Assert.Equal((ushort)2020, valid!.Year);
+ }
+
+ UtcDate? invalid = UtcDate.Ymd(2020, 13, 40);
+ Assert.Null(invalid);
+ }
+
+ // Enum passed in and returned by value across the boundary.
+ [Fact]
+ public void EnumByValue_HashAlgorithm()
+ {
+ using SignatureAlgorithm ecdsa = SignatureAlgorithm.NewEcdsa(HashAlgorithm.Sha2256);
+ Assert.NotNull(ecdsa);
+ }
+
+ // Result through a longer pipeline (Argon2 password hashing).
+ [Fact]
+ public void ResultReturningString_Argon2()
+ {
+ using Argon2Params parameters = Argon2Params.New();
+ parameters.SetMCost(8);
+ parameters.SetTCost(1);
+ parameters.SetPCost(1);
+ parameters.SetOutputLen(32);
+
+ using Argon2 argon2 = Argon2.New(Argon2Algorithm.Argon2id, parameters);
+ string hash = argon2.HashPassword("correct horse battery staple");
+ Assert.StartsWith("$argon2", hash);
+ }
+
+ // Full JWT encode/decode: builder setters (now methods), string returns,
+ // and object Results all in one flow.
+ [Fact]
+ public void JwtEncodeDecodeRoundTrip()
+ {
+ const string claims = "{\"sub\":\"net48\",\"admin\":true}";
+
+ using PrivateKey priv = PrivateKey.GenerateEc(EcCurve.NistP256);
+
+ using JwtSigBuilder builder = JwtSig.Builder();
+ builder.SetAlgorithm(JwsAlg.Es256);
+ builder.SetContentType("AUTH");
+ builder.SetClaims(claims);
+
+ using JwtSig signed = builder.Build();
+ string compact = signed.Encode(priv);
+ Assert.False(string.IsNullOrEmpty(compact));
+
+ using JwtSig decoded = JwtSig.DecodeUnchecked(compact);
+ Assert.Equal("AUTH", decoded.ContentType);
+ Assert.Contains("net48", decoded.Claims);
+ }
+
+ // VecU8: FromBytes, a value-return property, and Fill into a caller buffer
+ // (Result<(), Box> — the buffer-too-small error path).
+ [Fact]
+ public void VecU8_FillAndLength()
+ {
+ byte[] source = { 10, 20, 30, 40 };
+ using VecU8 vec = VecU8.FromBytes(source);
+ Assert.Equal((ulong)source.Length, (ulong)vec.Length);
+
+ byte[] buffer = new byte[source.Length];
+ vec.Fill(buffer);
+ Assert.Equal(source, buffer);
+
+ Assert.Throws(() => vec.Fill(new byte[1]));
+ }
+
+ // The error arm: a bad parse must come back as a typed PickyException whose
+ // payload (an opaque PickyError) is itself readable across the boundary.
+ [Fact]
+ public void ErrorArm_CarriesReadablePickyError()
+ {
+ PickyException ex = Assert.Throws(() => Pem.Parse("not a pem at all"));
+ Assert.NotNull(ex.Inner);
+ Assert.False(string.IsNullOrEmpty(ex.Inner.ToDisplay()));
+ // Kind is an enum getter property on the error opaque.
+ _ = ex.Inner.Kind;
+ }
+}
diff --git a/ffi/dotnet/Devolutions.Picky/Generated/AesAuthEncParams.cs b/ffi/dotnet/Devolutions.Picky/Generated/AesAuthEncParams.cs
index 79f0e6e0..f67fdf2b 100644
--- a/ffi/dotnet/Devolutions.Picky/Generated/AesAuthEncParams.cs
+++ b/ffi/dotnet/Devolutions.Picky/Generated/AesAuthEncParams.cs
@@ -1,11 +1,8 @@
// by Diplomat
-#pragma warning disable 0105
using System;
using System.Runtime.InteropServices;
-
using Devolutions.Picky.Diplomat;
-#pragma warning restore 0105
namespace Devolutions.Picky;
@@ -13,28 +10,60 @@ namespace Devolutions.Picky;
public partial class AesAuthEncParams: IDisposable
{
- private unsafe Raw.AesAuthEncParams* _inner;
+ private unsafe RustHandle _inner;
+
+ ///
+ /// Roots the wrappers this value borrows from so the GC cannot finalize
+ /// a borrowed-from parent while this value is alive.
+ ///
+ private object[] _edges;
+
+ private static readonly unsafe RustDestructor _destroy = Raw.AesAuthEncParams.Destroy;
///
/// Creates a managed AesAuthEncParams from a raw handle.
///
///
- /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free).
+ /// Safety: you should not build two managed objects using the same raw handle (may cause use-after-free and double-free).
///
/// This constructor assumes the raw struct is allocated on Rust side.
/// If implemented, the custom Drop implementation on Rust side WILL run on destruction.
///
- public unsafe AesAuthEncParams(Raw.AesAuthEncParams* handle)
+ internal unsafe AesAuthEncParams(Raw.AesAuthEncParams* handle)
+ {
+ _inner = RustHandle.Owned(handle, _destroy);
+ _edges = System.Array.Empty