From 5e24293622667310331433035dcce544e37b1b7e Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Sat, 3 Jan 2026 10:46:15 +0100 Subject: [PATCH] Protocol: add SkipSafeHandle class as a way to skip reading handles so other readers can still consume them. --- src/Tmds.DBus.Protocol/Reader.Handle.cs | 34 ++++----- src/Tmds.DBus.Protocol/SkipSafeHandle.cs | 32 +++++++++ src/Tmds.DBus.Protocol/UnixFdCollection.cs | 8 +++ src/Tmds.DBus.Protocol/VariantValue.cs | 7 +- test/Tmds.DBus.Protocol.Tests/ReaderTests.cs | 72 +++++++++++++++++++ .../VariantValueTests.cs | 33 +++++++++ 6 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 src/Tmds.DBus.Protocol/SkipSafeHandle.cs diff --git a/src/Tmds.DBus.Protocol/Reader.Handle.cs b/src/Tmds.DBus.Protocol/Reader.Handle.cs index 0ecb94f..e9abdcf 100644 --- a/src/Tmds.DBus.Protocol/Reader.Handle.cs +++ b/src/Tmds.DBus.Protocol/Reader.Handle.cs @@ -5,40 +5,40 @@ public ref partial struct Reader /// /// Reads a Unix file descriptor handle. /// - /// The SafeHandle type to read. - /// The handle, or null if unavailable. + /// The type to read. + /// The handle, or if is or if file descriptor passing is not supported. + /// + /// A handle can only be read once. + /// To skip reading a handle, call ReadHandle<SkipSafeHandle>(), which will return without consuming the underlying handle. + /// public T? ReadHandle<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]T>() where T : SafeHandle, new() => ReadHandleGeneric(); internal T? ReadHandleGeneric<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]T>() { int idx = (int)ReadUInt32(); - if (idx >= _handleCount) + if (_handles is null) { - throw new IndexOutOfRangeException(); + return default(T); } - if (_handles is not null) - { - return _handles.ReadHandleGeneric(idx); - } - return default(T); + return _handles.ReadHandleGeneric(idx); } /// /// Reads a Unix file descriptor handle as a raw IntPtr. /// - /// The handle is still owned (i.e. Disposed) by the . + /// + /// A handle can only be read once. + /// The handle is still owned (i.e. Disposed) by the . + /// To skip reading a handle, call ReadHandle<SkipSafeHandle>(), which will return without consuming the underlying handle. + /// public IntPtr ReadHandleRaw() { int idx = (int)ReadUInt32(); - if (idx >= _handleCount) - { - throw new IndexOutOfRangeException(); - } - if (_handles is not null) + if (_handles is null) { - return _handles.ReadHandleRaw(idx); + return new IntPtr(-1); } - return new IntPtr(-1); + return _handles.ReadHandleRaw(idx); } } diff --git a/src/Tmds.DBus.Protocol/SkipSafeHandle.cs b/src/Tmds.DBus.Protocol/SkipSafeHandle.cs new file mode 100644 index 0000000..5d6b5f0 --- /dev/null +++ b/src/Tmds.DBus.Protocol/SkipSafeHandle.cs @@ -0,0 +1,32 @@ +namespace Tmds.DBus.Protocol; + +/// +/// A SafeHandle that can be used to skip reading a Unix file descriptor handle. +/// +/// +/// When this type is used with handle reading methods, the handle will not be read +/// and will be returned instead. This allows skipping handles without consuming them. +/// +public sealed class SkipSafeHandle : SafeHandle +{ + /// + /// Initializes a new instance of the class. + /// + public SkipSafeHandle() : base(new IntPtr(-1), false) + { + } + + /// + /// Gets a value indicating whether the handle is invalid. + /// + public override bool IsInvalid => true; + + /// + /// Releases the handle. + /// + /// Always returns true. + protected override bool ReleaseHandle() + { + return true; + } +} diff --git a/src/Tmds.DBus.Protocol/UnixFdCollection.cs b/src/Tmds.DBus.Protocol/UnixFdCollection.cs index 3f08b3d..1bbc3df 100644 --- a/src/Tmds.DBus.Protocol/UnixFdCollection.cs +++ b/src/Tmds.DBus.Protocol/UnixFdCollection.cs @@ -111,6 +111,10 @@ private void ThrowDisposed() if (_rawHandles is not null) { (IntPtr rawHandle, bool CanRead) = _rawHandles[index]; + if (typeof(T) == typeof(SkipSafeHandle)) + { + return default; + } if (!CanRead) { ThrowHandleAlreadyRead(); @@ -128,6 +132,10 @@ private void ThrowDisposed() { Debug.Assert(_handles is not null); (SafeHandle? handle, bool CanRead) = _handles![index]; + if (typeof(T) == typeof(SkipSafeHandle)) + { + return default; + } if (!CanRead) { ThrowHandleAlreadyRead(); diff --git a/src/Tmds.DBus.Protocol/VariantValue.cs b/src/Tmds.DBus.Protocol/VariantValue.cs index a9bf26e..630b48b 100644 --- a/src/Tmds.DBus.Protocol/VariantValue.cs +++ b/src/Tmds.DBus.Protocol/VariantValue.cs @@ -805,6 +805,11 @@ private static void EnsureCanUnsafeGet(VariantValueType type) /// Reads a Unix file descriptor handle for a type. /// /// The type to read. + /// The handle, or if is or if file descriptor passing is not supported. + /// + /// A handle can only be read once. + /// To skip reading a handle, call ReadHandle<SkipSafeHandle>(), which will return without consuming the underlying handle. + /// public T? ReadHandle< #if NET6_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] @@ -2209,7 +2214,7 @@ private void WriteValueTo(ref MessageWriter writer, int nestingOffset) SafeHandle? handle = UnsafeReadHandle(); if (handle is null) { - throw new InvalidOperationException("Handle already read"); + throw new NotSupportedException("Handle not available."); } writer.WriteHandle(handle); break; diff --git a/test/Tmds.DBus.Protocol.Tests/ReaderTests.cs b/test/Tmds.DBus.Protocol.Tests/ReaderTests.cs index 48bc8d4..7e1263a 100644 --- a/test/Tmds.DBus.Protocol.Tests/ReaderTests.cs +++ b/test/Tmds.DBus.Protocol.Tests/ReaderTests.cs @@ -146,6 +146,78 @@ public void ReadHandleRaw() Assert.Equal(expected, handle); } + [Fact] + public void ReadHandle_SkipSafeHandle_ReturnsNull() + { + byte handleIndex = 0; + IntPtr expected = new IntPtr(-3); + using UnixFdCollection fds = new UnixFdCollection(isRawHandleCollection: true); + fds.AddHandle(expected); + byte[] bigEndianData = new byte[] { 0, 0, 0, handleIndex }; + + Reader reader = new Reader(isBigEndian: true, new System.Buffers.ReadOnlySequence(bigEndianData), handles: fds, fds.Count); + var skipped = reader.ReadHandle(); + + Assert.Null(skipped); + } + + [Fact] + public void ReadHandle_AfterSkip_CanStillReadHandle() + { + byte handleIndex = 0; + IntPtr expected = new IntPtr(-3); + using UnixFdCollection fds = new UnixFdCollection(isRawHandleCollection: true); + fds.AddHandle(expected); + byte[] bigEndianData = new byte[] { 0, 0, 0, handleIndex, 0, 0, 0, handleIndex }; + + Reader reader = new Reader(isBigEndian: true, new System.Buffers.ReadOnlySequence(bigEndianData), handles: fds, fds.Count); + + var skipped = reader.ReadHandle(); + Assert.Null(skipped); + + using var handle = reader.ReadHandle(); + Assert.NotNull(handle); + Assert.Equal(expected, handle.DangerousGetHandle()); + } + + [Fact] + public void ReadHandleRaw_AfterSkip_CanStillReadHandle() + { + byte handleIndex = 0; + IntPtr expected = new IntPtr(-3); + using UnixFdCollection fds = new UnixFdCollection(isRawHandleCollection: true); + fds.AddHandle(expected); + byte[] bigEndianData = new byte[] { 0, 0, 0, handleIndex, 0, 0, 0, handleIndex }; + + Reader reader = new Reader(isBigEndian: true, new System.Buffers.ReadOnlySequence(bigEndianData), handles: fds, fds.Count); + + var skipped = reader.ReadHandle(); + Assert.Null(skipped); + + IntPtr handle = reader.ReadHandleRaw(); + Assert.Equal(expected, handle); + } + + [Fact] + public void ReadHandle_WithNullHandles_ReturnsNull() + { + byte[] bigEndianData = new byte[] { 0, 0, 0, 0 }; + Reader reader = new Reader(isBigEndian: true, new System.Buffers.ReadOnlySequence(bigEndianData), handles: null, handleCount: 0); + + using var handle = reader.ReadHandle(); + Assert.Null(handle); + } + + [Fact] + public void ReadHandleRaw_WithNullHandles_ReturnsInvalidHandle() + { + byte[] bigEndianData = new byte[] { 0, 0, 0, 0 }; + Reader reader = new Reader(isBigEndian: true, new System.Buffers.ReadOnlySequence(bigEndianData), handles: null, handleCount: 0); + + IntPtr handle = reader.ReadHandleRaw(); + Assert.Equal(new IntPtr(-1), handle); + } + public bool Equals(VariantValue lhs, VariantValue other) { if (lhs.GetDBusSignature() != other.GetDBusSignature()) diff --git a/test/Tmds.DBus.Protocol.Tests/VariantValueTests.cs b/test/Tmds.DBus.Protocol.Tests/VariantValueTests.cs index eee30da..918e2ac 100644 --- a/test/Tmds.DBus.Protocol.Tests/VariantValueTests.cs +++ b/test/Tmds.DBus.Protocol.Tests/VariantValueTests.cs @@ -770,6 +770,39 @@ public void UnixFd(byte nesting) Assert.Equal(-1, vv.Count); } + [Fact] + public void UnixFd_SkipSafeHandle_ReturnsNull() + { + byte handleIndex = 0; + IntPtr expected = new IntPtr(-3); + using UnixFdCollection fds = new UnixFdCollection(isRawHandleCollection: true); + fds.AddHandle(expected); + + var vv = new VariantValue(fds, handleIndex); + var skipped = vv.ReadHandle(); + + Assert.Null(skipped); + } + + [Fact] + public void UnixFd_AfterSkip_CanStillReadHandle() + { + byte handleIndex = 0; + IntPtr expected = new IntPtr(-3); + using UnixFdCollection fds = new UnixFdCollection(isRawHandleCollection: true); + fds.AddHandle(expected); + + var vv1 = new VariantValue(fds, handleIndex); + var vv2 = new VariantValue(fds, handleIndex); + + var skipped = vv1.ReadHandle(); + Assert.Null(skipped); + + using var handle = vv2.ReadHandle(); + Assert.NotNull(handle); + Assert.Equal(expected, handle.DangerousGetHandle()); + } + private static VariantValue Nest(VariantValue vv, byte nesting) { for (int i = 0; i < nesting; i++)