Skip to content

Commit b8e19ee

Browse files
Fix Guid handling (Azure#33881)
* Fix Guid handling * Move to separate class * using * Optimize Guid creation * Optimize big endian write * Move to core * Rename * Fix * Update parse handling * Fix SB tests and revert Parse change * PR fb * Apply suggestions from code review Co-authored-by: Daniel Marbach <daniel.marbach@openplace.net> * Set out param * Add helper --------- Co-authored-by: danielmarbach <daniel.marbach@openplace.net>
1 parent 2557a4e commit b8e19ee

File tree

7 files changed

+180
-32
lines changed

7 files changed

+180
-32
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Buffers.Binary;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
9+
namespace Azure.Core.Shared;
10+
11+
/// <summary>
12+
/// The purpose of this class is to provide an allocation free way to parse and write Guids on .NET Standard 2.0, taking into account
13+
/// endianness.
14+
/// </summary>
15+
internal static class GuidUtilities
16+
{
17+
private const int GuidSizeInBytes = 16;
18+
19+
public static bool TryParseGuidBytes(ReadOnlySpan<byte> bytes, out Guid guid)
20+
{
21+
if (bytes.Length != GuidSizeInBytes)
22+
{
23+
guid = default;
24+
return false;
25+
}
26+
27+
if (BitConverter.IsLittleEndian)
28+
{
29+
guid = MemoryMarshal.Read<Guid>(bytes);
30+
return true;
31+
}
32+
33+
// copied from https://github.com/dotnet/runtime/blob/9129083c2fc6ef32479168f0555875b54aee4dfb/src/libraries/System.Private.CoreLib/src/System/Guid.cs#L49
34+
// slower path for BigEndian:
35+
byte k = bytes[15]; // hoist bounds checks
36+
int a = BinaryPrimitives.ReadInt32LittleEndian(bytes);
37+
short b = BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(4));
38+
short c = BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(6));
39+
byte d = bytes[8];
40+
byte e = bytes[9];
41+
byte f = bytes[10];
42+
byte g = bytes[11];
43+
byte h = bytes[12];
44+
byte i = bytes[13];
45+
byte j = bytes[14];
46+
47+
guid = new Guid(a, b, c, d, e, f, g, h, i, j, k);
48+
return true;
49+
}
50+
51+
public static void WriteGuidToBuffer(Guid guid, Span<byte> buffer)
52+
{
53+
AssertBufferSize(buffer);
54+
55+
// Based on https://github.com/dotnet/runtime/blob/9129083c2fc6ef32479168f0555875b54aee4dfb/src/libraries/System.Private.CoreLib/src/System/Guid.cs#L836
56+
57+
if (BitConverter.IsLittleEndian)
58+
{
59+
MemoryMarshal.Write(buffer, ref guid);
60+
return;
61+
}
62+
63+
GuidData data = Unsafe.As<Guid, GuidData>(ref guid);
64+
65+
buffer[15] = data.K; // hoist bounds checks
66+
BinaryPrimitives.WriteInt32LittleEndian(buffer, data.A);
67+
BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(4), data.B);
68+
BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(6), data.C);
69+
buffer[8] = data.D;
70+
buffer[9] = data.E;
71+
buffer[10] = data.F;
72+
buffer[11] = data.G;
73+
buffer[12] = data.H;
74+
buffer[13] = data.I;
75+
buffer[14] = data.J;
76+
}
77+
78+
private static void AssertBufferSize(Span<byte> buffer)
79+
{
80+
if (buffer.Length != GuidSizeInBytes)
81+
{
82+
throw new ArgumentException($"The length of the supplied buffer must be {GuidSizeInBytes}.", nameof(buffer));
83+
}
84+
}
85+
86+
// This struct has the fields layed out to be GUID-like in order to read the GUID fields
87+
// to efficiently write them into memory without having to deal with endianness
88+
// Do not rename or reorder the fields.
89+
private readonly struct GuidData
90+
{
91+
public readonly int A;
92+
public readonly short B;
93+
public readonly short C;
94+
public readonly byte D;
95+
public readonly byte E;
96+
public readonly byte F;
97+
public readonly byte G;
98+
public readonly byte H;
99+
public readonly byte I;
100+
public readonly byte J;
101+
public readonly byte K;
102+
103+
// Creates a new GUID like struct initialized to the value represented by the
104+
// arguments. The bytes are specified like this to avoid endianness issues.
105+
public GuidData(int a, short b, short c, byte d, byte e, byte f, byte g, byte h, byte i, byte j, byte k)
106+
{
107+
A = a;
108+
B = b;
109+
C = c;
110+
D = d;
111+
E = e;
112+
F = f;
113+
G = g;
114+
H = h;
115+
I = i;
116+
J = j;
117+
K = k;
118+
}
119+
}
120+
}

sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="..\..\Azure.Core.TestFramework\src\Instrumentation\ZeroPollingStrategy.cs" LinkBase="Shared\TestFramework" />
5050
<Compile Include="$(TestFrameworkSupportFiles)" LinkBase="Shared\TestFramework" />
5151
<Compile Include="..\src\Shared\ApiVersionString.cs" LinkBase="Shared" />
52+
<Compile Include="..\src\Shared\GuidUtilities.cs" LinkBase="Shared" />
5253
</ItemGroup>
5354
<ItemGroup>
5455
<Compile Update="PemReaderTests.Keys.cs" DependentUpon="PemReaderTests.cs" />
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Azure.Core.Shared;
6+
using NUnit.Framework;
7+
8+
namespace Azure.Core.Tests;
9+
10+
public class GuidUtilitiesTests
11+
{
12+
[Test]
13+
public void CanParseGuidBytes()
14+
{
15+
var input = Guid.NewGuid();
16+
17+
Assert.IsTrue(GuidUtilities.TryParseGuidBytes(input.ToByteArray(), out Guid output));
18+
Assert.AreEqual(input, output);
19+
}
20+
21+
[Test]
22+
public void TryParseGuidBytesReturnsFalseOnInvalidInput()
23+
{
24+
Assert.IsFalse(GuidUtilities.TryParseGuidBytes(new byte[15], out Guid guid));
25+
Assert.AreEqual(default(Guid), guid);
26+
}
27+
28+
[Test]
29+
public void CanWriteGuidBytes()
30+
{
31+
var input = Guid.NewGuid();
32+
var buffer = new byte[16];
33+
34+
GuidUtilities.WriteGuidToBuffer(input, buffer);
35+
CollectionAssert.AreEqual(input.ToByteArray(), buffer);
36+
}
37+
38+
[Test]
39+
public void WriteGuidBytesThrowsOnInvalidBuffer()
40+
{
41+
var input = Guid.NewGuid();
42+
var buffer = new byte[15];
43+
Assert.Throws<ArgumentException>(() => GuidUtilities.WriteGuidToBuffer(input, buffer));
44+
}
45+
}

sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpMessageConverter.cs

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Runtime.Serialization;
1111
using Azure.Core;
1212
using Azure.Core.Amqp;
13+
using Azure.Core.Shared;
1314
using Azure.Messaging.ServiceBus.Primitives;
1415
using Microsoft.Azure.Amqp;
1516
using Microsoft.Azure.Amqp.Encoding;
@@ -19,11 +20,6 @@ namespace Azure.Messaging.ServiceBus.Amqp
1920
{
2021
internal class AmqpMessageConverter
2122
{
22-
/// <summary>
23-
/// The size, in bytes, to use for extracting the delivery tag bytes into <see cref="Guid"/>.
24-
/// </summary>
25-
private const int GuidSizeInBytes = 16;
26-
2723
/// <summary>The size, in bytes, to use as a buffer for stream operations.</summary>
2824
private const int StreamBufferSizeInBytes = 512;
2925

@@ -554,33 +550,17 @@ public virtual ServiceBusReceivedMessage AmqpMessageToSBReceivedMessage(AmqpMess
554550
AmqpAnnotatedMessage annotatedMessage = AmqpMessageToAnnotatedMessage(amqpMessage, isPeeked);
555551

556552
ServiceBusReceivedMessage sbMessage = new ServiceBusReceivedMessage(annotatedMessage);
557-
558-
// lock token
559-
560-
sbMessage.LockTokenGuid = ParseGuidBytes(amqpMessage.DeliveryTag);
553+
if (GuidUtilities.TryParseGuidBytes(amqpMessage.DeliveryTag, out Guid lockToken))
554+
{
555+
// lock token
556+
sbMessage.LockTokenGuid = lockToken;
557+
};
561558

562559
amqpMessage.Dispose();
563560

564561
return sbMessage;
565562
}
566563

567-
public virtual Guid ParseGuidBytes(ReadOnlyMemory<byte> bytes)
568-
{
569-
if (bytes.Length == GuidSizeInBytes)
570-
{
571-
// Use TryRead to avoid allocating an array if we are on a little endian machine.
572-
if (!BitConverter.IsLittleEndian || !MemoryMarshal.TryRead<Guid>(bytes.Span, out var lockTokenGuid))
573-
{
574-
// Either we are on a big endian machine or the bytes were not a valid GUID.
575-
// Even if the bytes were not valid, use the Guid constructor to leverage the Guid validation rather than throwing ourselves.
576-
lockTokenGuid = new Guid(bytes.ToArray());
577-
}
578-
return lockTokenGuid;
579-
}
580-
581-
return default;
582-
}
583-
584564
internal static bool TryGetAmqpObjectFromNetObject(object netObject, MappingType mappingType, out object amqpObject)
585565
{
586566
amqpObject = null;

sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpReceiver.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
using System.Collections.Generic;
77
using System.Collections.ObjectModel;
88
using System.Runtime.ExceptionServices;
9-
using System.Runtime.InteropServices;
109
using System.Threading;
1110
using System.Threading.Tasks;
1211
using System.Transactions;
1312
using Azure.Core;
1413
using Azure.Core.Diagnostics;
14+
using Azure.Core.Shared;
1515
using Azure.Messaging.ServiceBus.Core;
1616
using Azure.Messaging.ServiceBus.Diagnostics;
1717
using Azure.Messaging.ServiceBus.Primitives;
@@ -476,10 +476,7 @@ private async Task DisposeMessageAsync(
476476
TimeSpan timeout)
477477
{
478478
byte[] bufferForLockToken = ArrayPool<byte>.Shared.Rent(SizeOfGuidInBytes);
479-
if (!MemoryMarshal.TryWrite(bufferForLockToken, ref lockToken))
480-
{
481-
lockToken.ToByteArray().AsSpan().CopyTo(bufferForLockToken);
482-
}
479+
GuidUtilities.WriteGuidToBuffer(lockToken, bufferForLockToken);
483480

484481
ArraySegment<byte> deliveryTag = new ArraySegment<byte>(bufferForLockToken, 0, SizeOfGuidInBytes);
485482
ReceivingAmqpLink receiveLink = null;

sdk/servicebus/Azure.Messaging.ServiceBus/src/Azure.Messaging.ServiceBus.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
<Compile Include="$(AzureCoreSharedSources)ValueStopwatch.cs" LinkBase="SharedSource\Azure.Core" />
4444
<Compile Include="$(AzureCoreSharedSources)PageResponseEnumerator.cs" LinkBase="SharedSource\Azure.Core" />
4545
<Compile Include="$(AzureCoreSharedSources)AzureResourceProviderNamespaceAttribute.cs" LinkBase="SharedSource\Azure.Core" />
46+
<Compile Include="$(AzureCoreSharedSources)GuidUtilities.cs" LinkBase="SharedSource\Azure.Core" />
4647
</ItemGroup>
4748

4849
<ItemGroup>

sdk/servicebus/Azure.Messaging.ServiceBus/src/Compatibility/ServiceBusAmqpExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using Azure.Core.Shared;
56
using Azure.Messaging.ServiceBus.Amqp;
67
using Microsoft.Azure.Amqp;
78
using AmqpLib = Microsoft.Azure.Amqp;
@@ -35,7 +36,10 @@ public static ServiceBusReceivedMessage FromAmqpBytes(BinaryData messageBytes, B
3536
var amqpMessage = AmqpLib.AmqpMessage.CreateInputMessage(bufferStream);
3637

3738
var message = AmqpMessageConverter.Default.AmqpMessageToSBReceivedMessage(amqpMessage);
38-
message.LockTokenGuid = AmqpMessageConverter.Default.ParseGuidBytes(lockTokenBytes);
39+
if (GuidUtilities.TryParseGuidBytes(lockTokenBytes, out Guid lockToken))
40+
{
41+
message.LockTokenGuid = lockToken;
42+
}
3943
return message;
4044
}
4145
}

0 commit comments

Comments
 (0)