Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions CredentialCache.Test/GErrorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.CredentialCache.Test;

using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using ktsu.CredentialCache.Storage;

/// <summary>
/// Covers reading the message out of a glib <c>GError</c>, which the Linux store does on
/// every libsecret failure. The GError here is hand-built, so this runs on every platform.
/// </summary>
[TestClass]
public class GErrorTests
{
// GError is { guint32 domain; gint code; gchar *message; }, so the message pointer
// sits at offset 8 on both 32-bit and 64-bit, with no padding before it.
private const int MessageOffset = sizeof(uint) + sizeof(int);

[TestMethod]
public void ReadMessageReturnsTheMessageField()
{
IntPtr message = Marshal.StringToCoTaskMemUTF8("Cannot autolaunch D-Bus without X11 $DISPLAY");

// One pointer longer than the struct and zeroed, so a read past the end of the struct
// finds a null pointer rather than whatever happens to follow the allocation.
int size = MessageOffset + (IntPtr.Size * 2);
IntPtr error = Marshal.AllocHGlobal(size);
try
{
Marshal.Copy(new byte[size], 0, error, size);
Marshal.WriteInt32(error, 0, 42);
Marshal.WriteInt32(error, sizeof(uint), 7);
Marshal.WriteIntPtr(error, MessageOffset, message);

Assert.AreEqual("Cannot autolaunch D-Bus without X11 $DISPLAY", GError.ReadMessage(error));
}
finally
{
Marshal.FreeHGlobal(error);
Marshal.FreeCoTaskMem(message);
}
}

[TestMethod]
public void ReadMessageReturnsNullForANullMessage()
{
int size = MessageOffset + (IntPtr.Size * 2);
IntPtr error = Marshal.AllocHGlobal(size);
try
{
Marshal.Copy(new byte[size], 0, error, size);

Assert.IsNull(GError.ReadMessage(error));
}
finally
{
Marshal.FreeHGlobal(error);
}
}

[TestMethod]
public void ThrowIfErrorReportsTheMessageOfARealGError()
{
if (!OperatingSystem.IsLinux())
{
Assert.Inconclusive("GError comes from glib, which is only loaded on Linux.");
return;
}

Check warning on line 69 in CredentialCache.Test/GErrorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_CredentialCache&issues=AaDrCzyPbPV0wMWtJFns&open=AaDrCzyPbPV0wMWtJFns&pullRequest=172

AssertThrowIfErrorReportsTheMessage();
}

// Built by glib itself rather than by hand, so this checks the real layout. glib frees
// the error inside ThrowIfError.
[SupportedOSPlatform("linux")]
private static void AssertThrowIfErrorReportsTheMessage()
{
IntPtr glib = NativeLibrary.Load("libglib-2.0.so.0");
GErrorNewLiteral newLiteral = Marshal.GetDelegateForFunctionPointer<GErrorNewLiteral>(
NativeLibrary.GetExport(glib, "g_error_new_literal"));
IntPtr error = newLiteral(1, 2, "Cannot autolaunch D-Bus without X11 $DISPLAY");

CredentialStoreException exception = Assert.ThrowsExactly<CredentialStoreException>(
() => LinuxSecretServiceCredentialStore.ThrowIfError(error, "secret_password_lookup_sync"));

Assert.AreEqual("secret_password_lookup_sync failed: Cannot autolaunch D-Bus without X11 $DISPLAY", exception.Message);
}

private delegate IntPtr GErrorNewLiteral(uint domain, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message);
}
29 changes: 29 additions & 0 deletions CredentialCache/Storage/GError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.CredentialCache.Storage;

using System.Runtime.InteropServices;

/// <summary>
/// glib's <c>GError</c>: <c>{ guint32 domain; gint code; gchar *message; }</c>.
/// </summary>
/// <remarks>
/// Declared rather than read at a hand-computed offset. The message pointer follows two
/// 4-byte fields, so it sits at offset 8 on both 32-bit and 64-bit; an offset derived from
/// <see cref="IntPtr.Size"/> reads past the end of the struct on 64-bit.
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
internal readonly struct GError
{
internal readonly uint Domain;
internal readonly int Code;
internal readonly IntPtr Message;

/// <summary>
/// Reads the message of the <c>GError</c> at <paramref name="error"/>.
/// </summary>
/// <param name="error">A non-null pointer to a <c>GError</c>.</param>
/// <returns>The message, or <see langword="null"/> if the error has none.</returns>
internal static string? ReadMessage(IntPtr error) =>
Marshal.PtrToStringUTF8(Marshal.PtrToStructure<GError>(error).Message);
}
5 changes: 2 additions & 3 deletions CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
Schema.Handle,
IntPtr.Zero,
ref error,
"service", _serviceName,

Check warning on line 56 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'service' 4 times.

Check warning on line 56 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'service' 4 times.

Check warning on line 56 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'service' 4 times.

Check warning on line 56 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'service' 4 times.

Check warning on line 56 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'service' 4 times.
"account", persona.ToString(),

Check warning on line 57 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'account' 4 times.

Check warning on line 57 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'account' 4 times.

Check warning on line 57 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'account' 4 times.

Check warning on line 57 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'account' 4 times.

Check warning on line 57 in CredentialCache/Storage/LinuxSecretServiceCredentialStore.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'account' 4 times.
IntPtr.Zero);

ThrowIfError(error, "secret_password_lookup_sync");
Expand Down Expand Up @@ -130,7 +130,7 @@
return removed;
}

private static void ThrowIfError(IntPtr error, string operation)
internal static void ThrowIfError(IntPtr error, string operation)
{
if (error == IntPtr.Zero)
{
Expand All @@ -139,8 +139,7 @@
string? message = null;
try
{
IntPtr messagePtr = Marshal.ReadIntPtr(error, IntPtr.Size * 2);
message = Marshal.PtrToStringUTF8(messagePtr);
message = GError.ReadMessage(error);
}
catch
{
Expand Down
Loading