diff --git a/libs/server/Resp/Bitmap/BitmapCommands.cs b/libs/server/Resp/Bitmap/BitmapCommands.cs index 34d948eb5a..e138cffcd4 100644 --- a/libs/server/Resp/Bitmap/BitmapCommands.cs +++ b/libs/server/Resp/Bitmap/BitmapCommands.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; @@ -10,6 +9,8 @@ namespace Garnet.server { + using SecondaryCommandList = List<(RespCommand, ArgSlice[])>; + /// (1) , (2) , (3) /// overflow check, ptr protection, and status not found implemented for below /// GETBIT, SETBIT, BITCOUNT, BITPOS (1),(2) @@ -359,7 +360,7 @@ private bool NetworkStringBitOperation(BitmapOperation bitOp, ref TG /// /// Performs arbitrary bitfield integer operations on strings. /// - private bool StringBitField(ref TGarnetApi storageApi, bool readOnly = false) + private bool StringBitField(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { if (parseState.Count < 1) @@ -371,12 +372,11 @@ private bool StringBitField(ref TGarnetApi storageApi, bool readOnly // Extract Key var sbKey = parseState.GetArgSliceByRef(0).SpanByte; - var currTokenIdx = 1; - var isOverflowTypeSet = false; ArgSlice overflowTypeSlice = default; - var secondaryCommandArgs = new List<(RespCommand, ArgSlice[])>(); + var secondaryCommandArgs = new SecondaryCommandList(); + var currTokenIdx = 1; while (currTokenIdx < parseState.Count) { // Get subcommand @@ -384,80 +384,56 @@ private bool StringBitField(ref TGarnetApi storageApi, bool readOnly var command = commandSlice.ReadOnlySpan; // Process overflow command - if (!readOnly && command.EqualsUpperCaseSpanIgnoringCase("OVERFLOW"u8)) + if (command.EqualsUpperCaseSpanIgnoringCase("OVERFLOW"u8)) { - // Get overflow parameter - overflowTypeSlice = parseState.GetArgSliceByRef(currTokenIdx); - isOverflowTypeSet = true; - // Validate overflow type - if (!parseState.TryGetBitFieldOverflow(currTokenIdx, out _)) + if (currTokenIdx >= parseState.Count || !parseState.TryGetBitFieldOverflow(currTokenIdx, out _)) { - while (!RespWriteUtils.TryWriteError( - $"ERR Overflow type {parseState.GetString(currTokenIdx)} not supported", - ref dcurr, dend)) + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_INVALID_OVERFLOW_TYPE, ref dcurr, dend)) SendAndReset(); return true; } - currTokenIdx++; + // Get overflow parameter + overflowTypeSlice = parseState.GetArgSliceByRef(currTokenIdx++); + isOverflowTypeSet = true; continue; } // [GET ] [SET ] [INCRBY ] // Process encoding argument - var encodingSlice = parseState.GetArgSliceByRef(currTokenIdx); - var offsetSlice = parseState.GetArgSliceByRef(currTokenIdx + 1); - var encodingArg = parseState.GetString(currTokenIdx); - var offsetArg = parseState.GetString(currTokenIdx + 1); - currTokenIdx += 2; - - // Validate encoding - if (encodingArg.Length < 2 || - (encodingArg[0] != 'i' && encodingArg[0] != 'u') || - !int.TryParse(encodingArg.AsSpan(1), out var bitCount) || - bitCount > 64 || - (bitCount == 64 && encodingArg[0] == 'u')) + if ((currTokenIdx >= parseState.Count) || !parseState.TryGetBitfieldEncoding(currTokenIdx, out _, out _)) { while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_INVALID_BITFIELD_TYPE, ref dcurr, dend)) SendAndReset(); return true; } + var encodingSlice = parseState.GetArgSliceByRef(currTokenIdx++); - // Validate offset - var isOffsetValid = offsetArg[0] == '#' - ? long.TryParse(offsetArg.AsSpan(1), out _) - : long.TryParse(offsetArg, out _); - - if (!isOffsetValid) + // Process offset argument + if ((currTokenIdx >= parseState.Count) || !parseState.TryGetBitfieldOffset(currTokenIdx, out _, out _)) { while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_BITOFFSET_IS_NOT_INTEGER, ref dcurr, dend)) SendAndReset(); return true; } + var offsetSlice = parseState.GetArgSliceByRef(currTokenIdx++); - // Subcommand takes 2 args, encoding and offset - if (command.EqualsUpperCaseSpanIgnoringCase("GET"u8)) + // GET Subcommand takes 2 args, encoding and offset + if (command.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GET)) { secondaryCommandArgs.Add((RespCommand.GET, [commandSlice, encodingSlice, offsetSlice])); } else { - if (readOnly) - { - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_SYNTAX_ERROR, ref dcurr, dend)) - SendAndReset(); - return true; - } - RespCommand op; // SET and INCRBY take 3 args, encoding, offset, and valueArg - if (command.EqualsUpperCaseSpanIgnoringCase("SET"u8)) + if (command.EqualsUpperCaseSpanIgnoringCase(CmdStrings.SET)) op = RespCommand.SET; - else if (command.EqualsUpperCaseSpanIgnoringCase("INCRBY"u8)) + else if (command.EqualsUpperCaseSpanIgnoringCase(CmdStrings.INCRBY)) op = RespCommand.INCRBY; else { @@ -469,24 +445,96 @@ private bool StringBitField(ref TGarnetApi storageApi, bool readOnly } // Validate value - var valueSlice = parseState.GetArgSliceByRef(currTokenIdx); - if (!parseState.TryGetLong(currTokenIdx, out _)) + if (currTokenIdx >= parseState.Count || !parseState.TryGetLong(currTokenIdx, out _)) { while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) SendAndReset(); return true; } + var valueSlice = parseState.GetArgSliceByRef(currTokenIdx); currTokenIdx++; secondaryCommandArgs.Add((op, [commandSlice, encodingSlice, offsetSlice, valueSlice])); } } + return StringBitFieldAction(ref storageApi, ref sbKey, RespCommand.BITFIELD, + secondaryCommandArgs, isOverflowTypeSet, overflowTypeSlice); + } + + /// + /// Performs arbitrary read-only bitfield integer operations + /// + private bool StringBitFieldReadOnly(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + if (parseState.Count < 1) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.BITFIELD_RO)); + } + + // BITFIELD_RO key [GET encoding offset [GET encoding offset] ... ] + // Extract Key + var sbKey = parseState.GetArgSliceByRef(0).SpanByte; + + var secondaryCommandArgs = new SecondaryCommandList(); + + var currTokenIdx = 1; + while (currTokenIdx < parseState.Count) + { + // Get subcommand + var commandSlice = parseState.GetArgSliceByRef(currTokenIdx++); + var command = commandSlice.ReadOnlySpan; + + // Read-only variant supports only GET subcommand + if (!command.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GET)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_SYNTAX_ERROR, ref dcurr, dend)) + SendAndReset(); + return true; + } + + // GET Subcommand takes 2 args, encoding and offset + + // Process encoding argument + if ((currTokenIdx >= parseState.Count) || !parseState.TryGetBitfieldEncoding(currTokenIdx, out _, out _)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_INVALID_BITFIELD_TYPE, ref dcurr, + dend)) + SendAndReset(); + return true; + } + var encodingSlice = parseState.GetArgSliceByRef(currTokenIdx++); + + // Process offset argument + if ((currTokenIdx >= parseState.Count) || !parseState.TryGetBitfieldOffset(currTokenIdx, out _, out _)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_BITOFFSET_IS_NOT_INTEGER, ref dcurr, + dend)) + SendAndReset(); + return true; + } + var offsetSlice = parseState.GetArgSliceByRef(currTokenIdx++); + + secondaryCommandArgs.Add((RespCommand.GET, [commandSlice, encodingSlice, offsetSlice])); + } + + return StringBitFieldAction(ref storageApi, ref sbKey, RespCommand.BITFIELD_RO, secondaryCommandArgs); + } + + private bool StringBitFieldAction(ref TGarnetApi storageApi, + ref SpanByte sbKey, + RespCommand cmd, + SecondaryCommandList secondaryCommandArgs, + bool isOverflowTypeSet = false, + ArgSlice overflowTypeSlice = default) + where TGarnetApi : IGarnetApi + { while (!RespWriteUtils.TryWriteArrayLength(secondaryCommandArgs.Count, ref dcurr, dend)) SendAndReset(); - var input = new RawStringInput(RespCommand.BITFIELD); + var input = new RawStringInput(cmd); for (var i = 0; i < secondaryCommandArgs.Count; i++) { @@ -512,7 +560,7 @@ private bool StringBitField(ref TGarnetApi storageApi, bool readOnly if (status == GarnetStatus.NOTFOUND && opCode == RespCommand.GET) { - while (!RespWriteUtils.TryWriteArrayItem(0, ref dcurr, dend)) + while (!RespWriteUtils.TryWriteInt32(0, ref dcurr, dend)) SendAndReset(); } else @@ -526,15 +574,5 @@ private bool StringBitField(ref TGarnetApi storageApi, bool readOnly return true; } - - /// - /// Performs arbitrary read-only bitfield integer operations - /// - private bool StringBitFieldReadOnly(ref TGarnetApi storageApi) - where TGarnetApi : IGarnetApi - { - // BITFIELD_RO key [GET encoding offset [GET encoding offset] ... ] - return StringBitField(ref storageApi, true); - } } } \ No newline at end of file diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitfield.cs b/libs/server/Resp/Bitmap/BitmapManagerBitfield.cs index 02d9d6c0f6..c991551253 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitfield.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitfield.cs @@ -441,5 +441,24 @@ public static (long, bool) BitFieldExecute(BitFieldCmdArgs args, byte* value, in _ => throw new GarnetException("BITFIELD secondary op not supported"), }; } + + /// + /// Execute readonly bitfield operation described at input on bitmap stored within value. + /// + /// + /// + /// + /// + public static long BitFieldExecute_RO(BitFieldCmdArgs args, byte* value, int valLen) + { + var bitCount = (byte)(args.typeInfo & 0x7F); + var signed = (args.typeInfo & (byte)BitFieldSign.SIGNED) > 0; + + return args.secondaryCommand switch + { + RespCommand.GET => GetBitfield(value, valLen, args.offset, bitCount, signed), + _ => throw new GarnetException("BITFIELD secondary op not supported"), + }; + } } } \ No newline at end of file diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index c21caee027..fec0d0c08a 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -148,6 +148,7 @@ static partial class CmdStrings public static ReadOnlySpan FIELDS => "FIELDS"u8; public static ReadOnlySpan TIMEOUT => "TIMEOUT"u8; public static ReadOnlySpan ERROR => "ERROR"u8; + public static ReadOnlySpan INCRBY => "INCRBY"u8; public static ReadOnlySpan NOGET => "NOGET"u8; /// @@ -244,6 +245,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_GT_LT_NX_NOT_COMPATIBLE => "ERR GT, LT, and/or NX options at the same time are not compatible"u8; public static ReadOnlySpan RESP_ERR_INCR_SUPPORTS_ONLY_SINGLE_PAIR => "ERR INCR option supports a single increment-element pair"u8; public static ReadOnlySpan RESP_ERR_INVALID_BITFIELD_TYPE => "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"u8; + public static ReadOnlySpan RESP_ERR_INVALID_OVERFLOW_TYPE => "ERR Invalid OVERFLOW type specified"u8; public static ReadOnlySpan RESP_ERR_SCRIPT_FLUSH_OPTIONS => "ERR SCRIPT FLUSH only support SYNC|ASYNC option"u8; public static ReadOnlySpan RESP_ERR_BUSSYKEY => "BUSYKEY Target key name already exists."u8; public static ReadOnlySpan RESP_ERR_LENGTH_AND_INDEXES => "If you want both the length and indexes, please just use IDX."u8; diff --git a/libs/server/SessionParseStateExtensions.cs b/libs/server/SessionParseStateExtensions.cs index 7bb451c488..38f8e91fa9 100644 --- a/libs/server/SessionParseStateExtensions.cs +++ b/libs/server/SessionParseStateExtensions.cs @@ -139,6 +139,84 @@ internal static bool TryGetBitFieldOverflow(this SessionParseState parseState, i return true; } + /// + /// Parse bit field ENCODING slice from parse state at specified index + /// + /// The parse state + /// The argument index + /// parsed bitcount + /// bitfield signtype + /// + internal static unsafe bool TryGetBitfieldEncoding(this SessionParseState parseState, int idx, out long bitCount, out bool isSigned) + { + bitCount = default; + isSigned = default; + var encodingSlice = parseState.GetArgSliceByRef(idx); + + if (encodingSlice.length <= 1) + { + return false; + } + + var ptr = encodingSlice.ptr + 1; + + isSigned = *encodingSlice.ptr == 'i'; + + if (!isSigned && *encodingSlice.ptr != 'u') + { + return false; + } + + return + RespReadUtils.TryReadInt64Safe(ref ptr, encodingSlice.ptr + encodingSlice.length, + out bitCount, out var bytesRead, + out _, out _, allowLeadingZeros: false) && + ((int)bytesRead == encodingSlice.length - 1) && (bytesRead > 0L) && + (bitCount > 0) && + ((isSigned && bitCount <= 64) || + (!isSigned && bitCount < 64)); + } + + /// + /// Parse bit field OFFSET slice from parse state at specified index + /// + /// The parse state + /// The argument index + /// parsed value + /// should value by multiplied by bitcount + /// + internal static unsafe bool TryGetBitfieldOffset(this SessionParseState parseState, int idx, out long bitFieldOffset, out bool multiplyOffset) + { + bitFieldOffset = default; + multiplyOffset = default; + var offsetSlice = parseState.GetArgSliceByRef(idx); + + if (offsetSlice.Length <= 0) + { + return false; + } + + var ptr = offsetSlice.ptr; + var len = offsetSlice.length; + + if (*ptr == '#') + { + if (offsetSlice.length == 1) + return false; + + multiplyOffset = true; + ptr++; + len--; + } + + return + RespReadUtils.TryReadInt64Safe(ref ptr, offsetSlice.ptr + offsetSlice.length, + out bitFieldOffset, out var bytesRead, + out _, out _, allowLeadingZeros: false) && + ((int)bytesRead == len) && (bytesRead > 0L) && + (bitFieldOffset >= 0); + } + /// /// Parse manager type from parse state at specified index /// diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 4d120e0fe4..619150df52 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -207,13 +207,23 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB case RespCommand.BITFIELD: var bitFieldArgs = GetBitFieldArguments(ref input); - var (retValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, value.ToPointer() + functionsState.etagState.etagSkippedStart, value.Length - functionsState.etagState.etagSkippedStart); + var (retValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, + value.ToPointer() + functionsState.etagState.etagSkippedStart, + value.Length - functionsState.etagState.etagSkippedStart); if (!overflow) CopyRespNumber(retValue, ref dst); else CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref dst); return; + case RespCommand.BITFIELD_RO: + var bitFieldArgs_RO = GetBitFieldArguments(ref input); + var retValue_RO = BitmapManager.BitFieldExecute_RO(bitFieldArgs_RO, + value.ToPointer() + functionsState.etagState.etagSkippedStart, + value.Length - functionsState.etagState.etagSkippedStart); + CopyRespNumber(retValue_RO, ref dst); + return; + case RespCommand.PFCOUNT: case RespCommand.PFMERGE: if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer(), value.Length)) @@ -756,15 +766,23 @@ BitFieldCmdArgs GetBitFieldArguments(ref RawStringInput input) // Get secondary command. Legal commands: GET, SET & INCRBY. var cmd = RespCommand.NONE; var sbCmd = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan; - if (sbCmd.EqualsUpperCaseSpanIgnoringCase("GET"u8)) + if (sbCmd.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GET)) cmd = RespCommand.GET; - else if (sbCmd.EqualsUpperCaseSpanIgnoringCase("SET"u8)) + else if (sbCmd.EqualsUpperCaseSpanIgnoringCase(CmdStrings.SET)) cmd = RespCommand.SET; - else if (sbCmd.EqualsUpperCaseSpanIgnoringCase("INCRBY"u8)) + else if (sbCmd.EqualsUpperCaseSpanIgnoringCase(CmdStrings.INCRBY)) cmd = RespCommand.INCRBY; - var encodingArg = input.parseState.GetString(currTokenIdx++); - var offsetArg = input.parseState.GetString(currTokenIdx++); + var bitfieldEncodingParsed = input.parseState.TryGetBitfieldEncoding( + currTokenIdx++, out var bitCount, out var isSigned); + Debug.Assert(bitfieldEncodingParsed); + var sign = isSigned ? (byte)BitFieldSign.SIGNED : (byte)BitFieldSign.UNSIGNED; + + // Calculate number offset from bitCount if offsetArg starts with # + var offsetParsed = input.parseState.TryGetBitfieldOffset(currTokenIdx++, out var offset, out var multiplyOffset); + Debug.Assert(offsetParsed); + if (multiplyOffset) + offset *= bitCount; long value = default; if (cmd == RespCommand.SET || cmd == RespCommand.INCRBY) @@ -780,15 +798,10 @@ BitFieldCmdArgs GetBitFieldArguments(ref RawStringInput input) overflowType = (byte)overflowTypeValue; } - var sign = encodingArg[0] == 'i' ? (byte)BitFieldSign.SIGNED : (byte)BitFieldSign.UNSIGNED; // Number of bits in signed number - var bitCount = (byte)int.Parse(encodingArg.AsSpan(1)); // At most 64 bits can fit into encoding info var typeInfo = (byte)(sign | bitCount); - // Calculate number offset from bitCount if offsetArg starts with # - var offset = offsetArg[0] == '#' ? long.Parse(offsetArg.AsSpan(1)) * bitCount : long.Parse(offsetArg); - return new BitFieldCmdArgs(cmd, typeInfo, offset, value, overflowType); } } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 033d32ddb4..6379a79633 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -194,6 +194,13 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); break; + case RespCommand.BITFIELD_RO: + var bitFieldArgs_RO = GetBitFieldArguments(ref input); + value.ShrinkSerializedLength(BitmapManager.LengthFromType(bitFieldArgs_RO)); + var bitfieldReturnValue_RO = BitmapManager.BitFieldExecute_RO(bitFieldArgs_RO, value.ToPointer(), value.Length); + CopyRespNumber(bitfieldReturnValue_RO, ref output); + break; + case RespCommand.SETRANGE: var offset = input.parseState.GetInt(0); var newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; @@ -690,6 +697,22 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyRespNumber(bitfieldReturnValue, ref output); break; + case RespCommand.BITFIELD_RO: + var bitFieldArgs_RO = GetBitFieldArguments(ref input); + v = value.ToPointer() + functionsState.etagState.etagSkippedStart; + + if (!BitmapManager.IsLargeEnoughForType(bitFieldArgs_RO, value.Length - functionsState.etagState.etagSkippedStart)) + return false; + + rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); + value.UnmarkExtraMetadata(); + value.ShrinkSerializedLength(value.Length + value.MetadataSize); + rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + + var bitfieldReturnValue_RO = BitmapManager.BitFieldExecute_RO(bitFieldArgs_RO, v, + value.Length - functionsState.etagState.etagSkippedStart); + CopyRespNumber(bitfieldReturnValue_RO, ref output); + break; case RespCommand.PFADD: v = value.ToPointer(); @@ -1220,7 +1243,9 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.BITFIELD: var bitFieldArgs = GetBitFieldArguments(ref input); Buffer.MemoryCopy(oldValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.Length - functionsState.etagState.etagSkippedStart, oldValue.Length - functionsState.etagState.etagSkippedStart); - var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, newValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.Length - functionsState.etagState.etagSkippedStart); + var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, + newValue.ToPointer() + functionsState.etagState.etagSkippedStart, + newValue.Length - functionsState.etagState.etagSkippedStart); if (!overflow) CopyRespNumber(bitfieldReturnValue, ref output); @@ -1228,6 +1253,17 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); break; + case RespCommand.BITFIELD_RO: + var bitFieldArgs_RO = GetBitFieldArguments(ref input); + Buffer.MemoryCopy(oldValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.Length - functionsState.etagState.etagSkippedStart, oldValue.Length - functionsState.etagState.etagSkippedStart); + var bitfieldReturnValue_RO = BitmapManager.BitFieldExecute_RO(bitFieldArgs_RO, + newValue.ToPointer() + functionsState.etagState.etagSkippedStart, + newValue.Length - functionsState.etagState.etagSkippedStart + ); + + CopyRespNumber(bitfieldReturnValue_RO, ref output); + break; + case RespCommand.PFADD: var updated = false; var newValPtr = newValue.ToPointer(); diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 076664954a..d432e576bd 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -76,6 +76,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) var bOffset = input.arg1; return sizeof(int) + BitmapManager.Length(bOffset); case RespCommand.BITFIELD: + case RespCommand.BITFIELD_RO: var bitFieldArgs = GetBitFieldArguments(ref input); return sizeof(int) + BitmapManager.LengthFromType(bitFieldArgs); case RespCommand.PFADD: @@ -178,6 +179,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) var bOffset = input.arg1; return sizeof(int) + BitmapManager.NewBlockAllocLength(t.Length, bOffset); case RespCommand.BITFIELD: + case RespCommand.BITFIELD_RO: var bitFieldArgs = GetBitFieldArguments(ref input); return sizeof(int) + BitmapManager.NewBlockAllocLengthFromType(bitFieldArgs, t.Length); case RespCommand.PFADD: diff --git a/test/Garnet.test/GarnetBitmapTests.cs b/test/Garnet.test/GarnetBitmapTests.cs index 61f61ae14b..98f771c79c 100644 --- a/test/Garnet.test/GarnetBitmapTests.cs +++ b/test/Garnet.test/GarnetBitmapTests.cs @@ -2521,13 +2521,153 @@ public void BitmapBitfieldBoundaryTest() bit = db.StringSetBit(key, offset: 8, bit: true); ClassicAssert.AreEqual(expected: false, actual: bit); - _ = db.Execute("BITFIELD", (RedisKey)key, "SET", "u8", 0, 1); - _ = db.Execute("BITFIELD", (RedisKey)key, "SET", "u8", 0, 128); - _ = db.Execute("BITFIELD", (RedisKey)key, "SET", "u8", 8, 1); + var ret = db.Execute("BITFIELD", (RedisKey)key, "SET", "u8", 0, 1); + ClassicAssert.AreEqual(1, ((string[])ret).Length); + ClassicAssert.AreEqual("128", ret[0].ToString()); + ret = db.Execute("BITFIELD", (RedisKey)key, "SET", "u8", 0, 128); + ClassicAssert.AreEqual(1, ((string[])ret).Length); + ClassicAssert.AreEqual("1", ret[0].ToString()); + + ret = db.Execute("BITFIELD", (RedisKey)key, "SET", "u8", 8, 1); + ClassicAssert.AreEqual(1, ((string[])ret).Length); + ClassicAssert.AreEqual("128", ret[0].ToString()); var result = (byte[])db.StringGet(key); ClassicAssert.AreEqual(expected: new byte[] { 0x80, 0x01 }, actual: result); + + ret = db.Execute("BITFIELD", (RedisKey)key, "SET", "u8", 8, 128, "GET", "u8", 8); + ClassicAssert.AreEqual(2, ((string[])ret).Length); + ClassicAssert.AreEqual("1", ret[0].ToString()); + ClassicAssert.AreEqual("128", ret[1].ToString()); + + result = (byte[])db.StringGet(key); + ClassicAssert.AreEqual(expected: new byte[] { 0x80, 0x80 }, actual: result); + } + + [Order(41)] + [Test] + [Category("BITFIELD")] + public void BitmapBitFieldInvalidOptionsTest([Values(RespCommand.BITFIELD, RespCommand.BITFIELD_RO)] RespCommand testCmd) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "BitmapBitFieldInvalidOptionsTest"; + + try + { + db.Execute(testCmd.ToString(), key, "GET"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is", + ex.Message); + } + + try + { + db.Execute(testCmd.ToString(), key, "GET", "u64", "0"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is", + ex.Message); + } + + try + { + db.Execute(testCmd.ToString(), key, "GET", "i-1", "0"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is", + ex.Message); + } + + try + { + db.Execute(testCmd.ToString(), key, "GET", "u8", @""""); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR bit offset is not an integer or out of range", + ex.Message); + } + + try + { + db.Execute(testCmd.ToString(), key, "GET", "i16", "#"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR bit offset is not an integer or out of range", + ex.Message); + } + + try + { + db.Execute(testCmd.ToString(), key, "GET", "32", "1"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is", + ex.Message); + } + + try + { + db.Execute(testCmd.ToString(), key, "GET", "u32", @"-1"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR bit offset is not an integer or out of range", + ex.Message); + } + + if (testCmd == RespCommand.BITFIELD) + { + try + { + db.Execute(testCmd.ToString(), key, "SET", "i32", "0"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR value is not an integer or out of range.", + ex.Message); + } + + try + { + db.Execute(testCmd.ToString(), key, "OVERFLOW", "NONE"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR Invalid OVERFLOW type specified", + ex.Message); + } + } + else + { + try + { + db.Execute(testCmd.ToString(), key, "SET", "i64", "0"); + Assert.Fail("Should be unreachable, arguments are incorrect"); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual("ERR syntax error", + ex.Message); + } + } } } } \ No newline at end of file