Skip to content

Commit d753b4d

Browse files
committed
NCBC-3852: Touch operations do not return MutationTokens
Motivation: When GetAndTouch is used on a document of "[12345]", TryReadMutationToken is used and fails Modifications: * Use sizeof() in TryReadMutationToken to guard against improper reads. * Replace magic numbers. * GetT and Touch operations read Expiry (uint32) from Extras in response, rather than trying to read a mutation token. * Since Touch and GetT don't inherit from the same base type, use static methods in Touch rather than inheritance to share implementation. Change-Id: I633d1198b3e3fe3ca99cc76e9676098c60f84594 Reviewed-on: https://review.couchbase.org/c/couchbase-net-client/+/215524 Reviewed-by: Brant Burnett <[email protected]> Reviewed-by: Jeffry Morris <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent 7131c39 commit d753b4d

File tree

5 files changed

+109
-8
lines changed

5 files changed

+109
-8
lines changed

src/Couchbase/Core/IO/Operations/GetT.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ internal GetT(string bucketName, string key) : base(bucketName, key)
1010

1111
protected override void WriteExtras(OperationBuilder builder)
1212
{
13-
Span<byte> extras = stackalloc byte[4];
14-
ByteConverter.FromUInt32(Expires, extras);
15-
builder.Write(extras);
13+
Touch.WriteExpiry(builder, Expires);
1614
}
1715

1816
protected override void WriteBody(OperationBuilder builder)
1917
{
2018
}
2119

20+
protected override void ReadExtras(ReadOnlySpan<byte> buffer)
21+
{
22+
// do not call MutationOperationBase.ReadExtras, as Touch operations do not contain MutationToken
23+
if (Touch.TryReadNewExpiry(buffer, Header.ExtrasLength, Header.ExtrasOffset, out var newExpiry))
24+
{
25+
Expires = newExpiry;
26+
}
27+
}
28+
2229
public override OpCode OpCode => OpCode.GAT;
2330
}
2431
}

src/Couchbase/Core/IO/Operations/OperationBase.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,15 @@ protected void TryReadServerDuration(ReadOnlySpan<byte> buffer)
383383

384384
protected void TryReadMutationToken(ReadOnlySpan<byte> buffer)
385385
{
386-
if (buffer.Length >= 40 && VBucketId.HasValue)
386+
var extrasOffset = Header.ExtrasOffset;
387+
var extrasLength = Header.ExtrasLength;
388+
var bufferMinLength = extrasOffset + extrasLength;
389+
if (buffer.Length >= bufferMinLength
390+
&& VBucketId.HasValue
391+
&& extrasLength >= (sizeof(Int64)*2))
387392
{
388-
var uuid = ByteConverter.ToInt64(buffer.Slice(Header.ExtrasOffset));
389-
var seqno = ByteConverter.ToInt64(buffer.Slice(Header.ExtrasOffset + 8));
393+
var uuid = ByteConverter.ToInt64(buffer.Slice(extrasOffset));
394+
var seqno = ByteConverter.ToInt64(buffer.Slice(extrasOffset + sizeof(Int64)));
390395
MutationToken = new MutationToken(BucketName, VBucketId.Value, uuid, seqno);
391396
}
392397
}

src/Couchbase/Core/IO/Operations/Touch.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,44 @@ namespace Couchbase.Core.IO.Operations
55
{
66
internal class Touch : MutationOperationBase
77
{
8-
protected override void WriteExtras(OperationBuilder builder)
8+
internal static void WriteExpiry(OperationBuilder builder, uint expires)
99
{
1010
Span<byte> extras = stackalloc byte[4];
11-
ByteConverter.FromUInt32(Expires, extras);
11+
ByteConverter.FromUInt32(expires, extras);
1212
builder.Write(extras);
1313
}
1414

15+
internal static bool TryReadNewExpiry(ReadOnlySpan<byte> buffer, int extrasLength, int extrasOffset, out uint expiry)
16+
{
17+
if (extrasLength >= sizeof(uint) && buffer.Length >= extrasOffset + extrasLength)
18+
{
19+
var expiryInExtras = buffer.Slice(extrasOffset);
20+
expiry = ByteConverter.ToUInt32(expiryInExtras);
21+
return true;
22+
}
23+
24+
expiry = 0;
25+
return false;
26+
}
27+
28+
protected override void WriteExtras(OperationBuilder builder)
29+
{
30+
WriteExpiry(builder, Expires);
31+
}
32+
1533
protected override void WriteBody(OperationBuilder builder)
1634
{
1735
}
1836

37+
protected override void ReadExtras(ReadOnlySpan<byte> buffer)
38+
{
39+
// do not call MutationOperationBase.ReadExtras, as Touch operations do not contain MutationToken
40+
if (Touch.TryReadNewExpiry(buffer, Header.ExtrasLength, Header.ExtrasOffset, out var newExpiry))
41+
{
42+
Expires = newExpiry;
43+
}
44+
}
45+
1946
public override OpCode OpCode => OpCode.Touch;
2047
}
2148
}

tests/Couchbase.UnitTests/Core/IO/Operations/OperationBaseTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
using System;
12
using System.Threading.Tasks;
23
using Couchbase.Core.IO.Operations;
4+
using Couchbase.UnitTests.Helpers;
5+
using Couchbase.Utils;
36
using Xunit;
47

58
namespace Couchbase.UnitTests.Core.IO.Operations
@@ -50,6 +53,36 @@ public async Task StopOperation_CalledTwice_KeepsOriginalTime()
5053
Assert.Equal(elapsed, operation.Elapsed);
5154
}
5255

56+
[Fact]
57+
public void OperationBase_ReadMutationTokenDoesNotThrowOnShortExtras()
58+
{
59+
// NCBC-3852
60+
var responseBytes = new byte[47];
61+
var fakeOp = new FakeMutationOperation()
62+
{
63+
VBucketId = 0x1,
64+
Header = new OperationHeader()
65+
{
66+
ExtrasLength = 4,
67+
FramingExtrasLength = 3,
68+
BodyLength = 23,
69+
}
70+
};
71+
72+
fakeOp.ForceTryReadMutationToken(responseBytes.AsSpan());
73+
Assert.Null(fakeOp.MutationToken);
74+
}
75+
76+
private class FakeMutationOperation : MutationOperationBase
77+
{
78+
public override OpCode OpCode => OpCode.Delete;
79+
80+
public void ForceTryReadMutationToken(ReadOnlySpan<byte> buffer)
81+
{
82+
TryReadMutationToken(buffer);
83+
}
84+
}
85+
5386
private class FakeOperation : OperationBase
5487
{
5588
public override OpCode OpCode => OpCode.Get;

tests/Couchbase.UnitTests/KeyValue/GetResultTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Couchbase.Core.Sharding;
1111
using Couchbase.KeyValue;
1212
using Couchbase.UnitTests.Helpers;
13+
using Couchbase.Utils;
1314
using Microsoft.Extensions.Logging;
1415
using Moq;
1516
using Xunit;
@@ -241,5 +242,33 @@ public void Test_OperationBase_Throws_InvalidArgumentException_When_Key_Is_Null(
241242
Assert.IsType<InvalidArgumentException>(e);
242243
}
243244
}
245+
246+
[Fact]
247+
public void Test_GetAndTouch_ReadsExpiryNotMutationToken()
248+
{
249+
// NCBC-3852
250+
byte[] responseBytes =
251+
[
252+
0x18, 0x1d, 0x03, 0x00,
253+
0x04, 0x01, 0x00, 0x00,
254+
0x00, 0x00, 0x00, 0x17,
255+
0x1c, 0x00, 0x00, 0x00,
256+
0x17, 0xf1, 0xc7, 0xcb,
257+
0xf7, 0x9f, 0x00, 0x00,
258+
0x02, 0x00, 0x0d, 0x02,
259+
0x00, 0x00, 0x01, 0x5b,
260+
0x22, 0x41, 0x42, 0x43,
261+
0x44, 0x45, 0x31, 0x32,
262+
0x33, 0x34, 0x35, 0x36,
263+
0x37, 0x22, 0x5d
264+
];
265+
266+
var getAndTouch = new GetT<byte[]>("default", "Test123");
267+
getAndTouch.Expires = 1234567;
268+
SlicedMemoryOwner<byte> responsePacket = new(new FakeMemoryOwner<byte>(new Memory<byte>(responseBytes)));
269+
getAndTouch.Read(responsePacket);
270+
Assert.NotEqual((uint)1234567, getAndTouch.Expires);
271+
Assert.Null(getAndTouch.MutationToken);
272+
}
244273
}
245274
}

0 commit comments

Comments
 (0)