diff --git a/libs/client/ClientSession/GarnetClientSession.cs b/libs/client/ClientSession/GarnetClientSession.cs index c822a17de21..a75af6cc0a7 100644 --- a/libs/client/ClientSession/GarnetClientSession.cs +++ b/libs/client/ClientSession/GarnetClientSession.cs @@ -16,7 +16,7 @@ namespace Garnet.client { /// - /// Mono-threaded remote client session for Garnet (a session makes a single network connection, and + /// Mono-threaded remote client session for Garnet (a session makes a single network connection, and /// expects mono-threaded client access, i.e., no concurrent invocations of API by client) /// public sealed partial class GarnetClientSession : IServerHook, IMessageConsumer @@ -44,7 +44,7 @@ public sealed partial class GarnetClientSession : IServerHook, IMessageConsumer Socket socket; int disposed; - // Send + // Send unsafe byte* offset, end; // Num outstanding commands @@ -103,6 +103,7 @@ public sealed partial class GarnetClientSession : IServerHook, IMessageConsumer /// Settings for send and receive network buffers /// Buffer pool to use for allocating send and receive buffers /// Max outstanding network sends allowed + /// Recieve result as raw string /// Logger public GarnetClientSession( EndPoint endpoint, @@ -431,6 +432,7 @@ public void Wait() private unsafe void InternalExecute(params string[] command) { byte* curr = offset; + while (!RespWriteUtils.TryWriteArrayLength(command.Length, ref curr, end)) { Flush(); diff --git a/libs/common/AsciiUtils.cs b/libs/common/AsciiUtils.cs index bd2f48b1b5b..42d6b1166ee 100644 --- a/libs/common/AsciiUtils.cs +++ b/libs/common/AsciiUtils.cs @@ -30,6 +30,22 @@ public static bool IsBetween(byte c, char minInclusive, char maxInclusive) return (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); } + /// Indicates whether character is ASCII quote character. + /// The character to evaluate. + /// true if is an ASCII quote character; otherwise, false. + public static bool IsQuoteChar(byte c) + { + return (c == '"') || (c == '\''); + } + + /// Indicates whether character is ASCII whitespace or a carriage return. + /// The character to evaluate. + /// true if is an ASCII whitespace character; otherwise, false. + public static bool IsRedisWhiteSpace(byte c) + { + return (c == ' ') || (c == '\t') || (c == '\r'); + } + public static byte ToLower(byte c) { if (IsBetween(c, 'A', 'Z')) diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index b7826d8f79c..6b49ab306dd 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -562,6 +562,10 @@ internal sealed class Options : ICloneable [Option("enable-debug-command", Required = false, HelpText = "Enable DEBUG command for 'no', 'local' or 'all' connections")] public ConnectionProtectionOption EnableDebugCommand { get; set; } + [OptionValidation] + [Option("enable-inline-command", Required = false, HelpText = "Enable inline commands for 'no', 'local' or 'all' connections")] + public ConnectionProtectionOption EnableInlineCommands { get; set; } + [OptionValidation] [Option("enable-module-command", Required = false, HelpText = "Enable MODULE command for 'no', 'local' or 'all' connections. Command can only load from paths listed in ExtensionBinPaths")] public ConnectionProtectionOption EnableModuleCommand { get; set; } @@ -922,6 +926,7 @@ public GarnetServerOptions GetServerOptions(ILogger logger = null) RevivInChainOnly = RevivInChainOnly.GetValueOrDefault(), RevivObjBinRecordCount = RevivObjBinRecordCount, EnableDebugCommand = EnableDebugCommand, + EnableInlineCommands = EnableInlineCommands, EnableModuleCommand = EnableModuleCommand, ExtensionBinPaths = FileUtils.ConvertToAbsolutePaths(ExtensionBinPaths), ExtensionAllowUnsignedAssemblies = ExtensionAllowUnsignedAssemblies.GetValueOrDefault(), diff --git a/libs/host/defaults.conf b/libs/host/defaults.conf index af7be040685..b42b55364dd 100644 --- a/libs/host/defaults.conf +++ b/libs/host/defaults.conf @@ -377,7 +377,10 @@ /* Enable DEBUG command for clients - no/local/yes */ "EnableDebugCommand": "no", - /* Enable DEBUG command for clients - no/local/yes */ + /* Enable inline commands for clients - no/local/yes */ + "EnableInlineCommands": "local", + + /* Enable MODULE command for clients - no/local/yes */ "EnableModuleCommand": "no", /* Protected mode */ diff --git a/libs/server/ArgSlice/ArgSliceUtils.cs b/libs/server/ArgSlice/ArgSliceUtils.cs index b418ef77cad..07ed4815ba8 100644 --- a/libs/server/ArgSlice/ArgSliceUtils.cs +++ b/libs/server/ArgSlice/ArgSliceUtils.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System; using Garnet.common; namespace Garnet.server @@ -10,10 +11,160 @@ namespace Garnet.server /// public static class ArgSliceUtils { + private static ReadOnlySpan CharToHexLookup => + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 + 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 + 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 + ]; + /// /// Compute hash slot of given ArgSlice /// public static unsafe ushort HashSlot(ref ArgSlice argSlice) => HashSlotUtils.HashSlot(argSlice.ptr, argSlice.Length); + + /// + /// Takes a quoted string from given ArgSlice and unescapes it. Destructive: Writes over the input memory. + /// + /// + /// + /// See TryParseInlineCommandArguments() for the quoting/escaping rules + /// + public static unsafe ArgSlice Unescape(ArgSlice slice, bool acceptUppercaseEscapes = false) + { + // Too short for quoting. + if (slice.Length <= 1) + { + return slice; + } + + // Get the last character to know if this is a quoted context. + var type = slice.Span[slice.Length - 1]; + + // Nothing to do if it's not. + if (!AsciiUtils.IsQuoteChar(type)) + { + return slice; + } + + // It's a quoted context, so we need to check for escapes. + + // Too short for escaping + if (slice.Length <= 3) + { + return new ArgSlice(slice.ptr + 1, slice.Length - 2); + } + + // How many bytes do we need to shift thanks to quoting and escaping. + var shift = 0; + // start offset + var start = 0; + // Are we in a quoting context? + var qStart = false; + + // Optimize command case by changing our start point instead of shifting the entire string. + if (slice.Span[0] == type) + { + qStart = true; + start = 1; + } + + for (var j = start; j < slice.Span.Length - shift - 1; ++j) + { + if (!qStart) + { + if (slice.Span[j + shift] == type) + { + qStart = true; + shift++; + slice.Span[j] = slice.Span[j + 1]; + } + continue; + } + + if (slice.Span[j + shift] != '\\') + { + // If we shifted earlier, we need to shift the rest too. + if (shift > 0) + slice.Span[j] = slice.Span[j + shift]; + continue; + } + + // ' context recognize only this particular sequence + if (type == '\'') + { + if (slice.Span[j + shift + 1] == '\'') + shift++; + if (shift > 0) + slice.Span[j] = slice.Span[j + shift]; + continue; + } + + // Process escapes + shift++; + var c = acceptUppercaseEscapes ? + (char)AsciiUtils.ToLower(slice.Span[j + shift]) : + (char)slice.Span[j + shift]; + + switch (c) + { + case 'a': + c = '\a'; + break; + case 'b': + c = '\b'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'x': + if (j + shift + 2 < slice.Span.Length) + { + var val = + 16 * CharToHexLookup[slice.ReadOnlySpan[j + shift + 1]] + + CharToHexLookup[slice.ReadOnlySpan[j + shift + 2]]; + + if (val < 0xFF) + { + c = (char)val; + shift += 2; + } + break; + } + break; + default: + break; + } + + slice.Span[j] = (byte)c; + } + + // Zero unnecessary chars + slice.Span[(slice.Span.Length - shift - start)..(slice.Span.Length - 1)].Clear(); + + // The final cut must remove 1 from length to trim the end quote character. + // (The starting quote character was already shifted out) + return new ArgSlice(slice.ptr + start, slice.Span.Length - 1 - shift - start); + } } } \ No newline at end of file diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index a81fec8caeb..935ef04915e 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1751,8 +1751,8 @@ private static void WriteClientInfo(IClusterProvider provider, StringBuilder int bool ParseGETAndKey(ref SpanByte key) { var oldEndReadHead = readHead = endReadHead; - var cmd = ParseCommand(writeErrorOnFailure: true, out var success); - if (!success || cmd != RespCommand.GET) + var cmd = ParseCommand(writeErrorOnFailure: true, out var commandReceived); + if (!commandReceived || cmd != RespCommand.GET) { // If we either find no command or a different command, we back off endReadHead = readHead = oldEndReadHead; diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index e0a4a29eb77..1486ff8ad03 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -202,6 +202,7 @@ static partial class CmdStrings /// public static ReadOnlySpan RESP_ERR_GENERIC_UNK_CMD => "ERR unknown command"u8; public static ReadOnlySpan RESP_ERR_NOT_SUPPORTED_RESP2 => "ERR command not supported in RESP2"u8; + public static ReadOnlySpan RESP_ERR_UNBALANCED_QUOTES => "ERR Protocol error: unbalanced quotes in request"u8; public static ReadOnlySpan RESP_ERR_GENERIC_CLUSTER_DISABLED => "ERR This instance has cluster support disabled"u8; public static ReadOnlySpan RESP_ERR_LUA_DISABLED => "ERR This instance has Lua scripting support disabled"u8; public static ReadOnlySpan RESP_ERR_GENERIC_WRONG_ARGUMENTS => "ERR wrong number of arguments for 'config|set' command"u8; @@ -428,7 +429,7 @@ static partial class CmdStrings public static ReadOnlySpan NO => "NO"u8; // Cluster subcommands which are internal and thus undocumented - // + // // Because these are internal, they have lower case property names public static ReadOnlySpan gossip => "GOSSIP"u8; public static ReadOnlySpan myparentid => "MYPARENTID"u8; diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 9d4224d56c8..6d6d74dd96f 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -8,7 +8,9 @@ using System.Runtime.InteropServices; using System.Text; using Garnet.common; +using Garnet.common.Parsing; using Microsoft.Extensions.Logging; +using Tsavorite.core; namespace Garnet.server { @@ -641,55 +643,19 @@ enum RespCommandOption : byte /// internal sealed unsafe partial class RespServerSession : ServerSessionBase { - /// - /// Fast-parses command type for inline RESP commands, starting at the current read head in the receive buffer - /// and advances read head. - /// - /// Outputs the number of arguments stored with the command. - /// RespCommand that was parsed or RespCommand.NONE, if no command was matched in this pass. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private RespCommand FastParseInlineCommand(out int count) - { - byte* ptr = recvBufferPtr + readHead; - count = 0; - - if (bytesRead - readHead >= 6) - { - if ((*(ushort*)(ptr + 4) == MemoryMarshal.Read("\r\n"u8))) - { - // Optimistically increase read head - readHead += 6; - - if ((*(uint*)ptr) == MemoryMarshal.Read("PING"u8)) - { - return RespCommand.PING; - } - - if ((*(uint*)ptr) == MemoryMarshal.Read("QUIT"u8)) - { - return RespCommand.QUIT; - } - - // Decrease read head, if no match was found - readHead -= 6; - } - } - - return RespCommand.NONE; - } + private static readonly ushort CrLf = MemoryMarshal.Read("\r\n"u8); /// /// Fast-parses for command type, starting at the current read head in the receive buffer /// and advances the read head to the position after the parsed command. /// /// Outputs the number of arguments stored with the command + /// The current read head to continue reading from + /// Bytes remaining in the read buffer /// RespCommand that was parsed or RespCommand.NONE, if no command was matched in this pass. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private RespCommand FastParseCommand(out int count) + private RespCommand FastParseCommand(out int count, byte* ptr, int remainingBytes) { - var ptr = recvBufferPtr + readHead; - var remainingBytes = bytesRead - readHead; - // Check if the package starts with "*_\r\n$_\r\n" (_ = masked out), // i.e. an array with a single-digit length and single-digit first string length. if ((remainingBytes >= 8) && (*(ulong*)ptr & 0xFFFF00FFFFFF00FF) == MemoryMarshal.Read("*\0\r\n$\0\r\n"u8)) @@ -790,10 +756,6 @@ static RespCommand MatchedNone(RespServerSession session, int oldReadHead) } } } - else - { - return FastParseInlineCommand(out count); - } // Couldn't find a matching command in this pass count = -1; @@ -806,15 +768,13 @@ static RespCommand MatchedNone(RespServerSession session, int oldReadHead) /// the parsed command/subcommand name. /// /// Reference to the number of remaining tokens in the packet. Will be reduced to number of command arguments. + /// The current read head to continue reading from + /// Bytes remaining in the read buffer + /// /// The parsed command name. - private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan specificErrorMessage) + private RespCommand FastParseArrayCommand(ref int count, byte* ptr, int remainingBytes, + ref ReadOnlySpan specificErrorMessage) { - // Bytes remaining in the read buffer - int remainingBytes = bytesRead - readHead; - - // The current read head to continue reading from - byte* ptr = recvBufferPtr + readHead; - // // Fast-path parsing by (1) command string length, (2) First character of command name (optional) and (3) priority (manual order) // @@ -1445,47 +1405,47 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan } break; case 8: - if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("ZREVRANK"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("ZREVRANK"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.ZREVRANK; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("SMEMBERS"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("SMEMBERS"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.SMEMBERS; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("BITFIELD"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("BITFIELD"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.BITFIELD; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("EXPIREAT"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("EXPIREAT"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.EXPIREAT; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("HPEXPIRE"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("HPEXPIRE"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.HPEXPIRE; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("HPERSIST"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("HPERSIST"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.HPERSIST; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("ZPEXPIRE"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("ZPEXPIRE"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.ZPEXPIRE; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("ZPERSIST"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("ZPERSIST"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.ZPERSIST; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("BZPOPMAX"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("BZPOPMAX"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.BZPOPMAX; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("BZPOPMIN"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("BZPOPMIN"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.BZPOPMIN; } - else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("SPUBLISH"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("SPUBLISH"u8) && *(ushort*)(ptr + 12) == CrLf) { return RespCommand.SPUBLISH; } @@ -1692,22 +1652,22 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan break; case 14: - if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\r\nZREMRA"u8) && *(ulong*)(ptr + 11) == MemoryMarshal.Read("NGEBYLEX"u8) && *(ushort*)(ptr + 19) == MemoryMarshal.Read("\r\n"u8)) + if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\r\nZREMRA"u8) && *(ulong*)(ptr + 11) == MemoryMarshal.Read("NGEBYLEX"u8) && *(ushort*)(ptr + 19) == CrLf) { return RespCommand.ZREMRANGEBYLEX; } - else if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\r\nGEOSEA"u8) && *(ulong*)(ptr + 11) == MemoryMarshal.Read("RCHSTORE"u8) && *(ushort*)(ptr + 19) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\r\nGEOSEA"u8) && *(ulong*)(ptr + 11) == MemoryMarshal.Read("RCHSTORE"u8) && *(ushort*)(ptr + 19) == CrLf) { return RespCommand.GEOSEARCHSTORE; } - else if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\r\nZREVRA"u8) && *(ulong*)(ptr + 11) == MemoryMarshal.Read("NGEBYLEX"u8) && *(ushort*)(ptr + 19) == MemoryMarshal.Read("\r\n"u8)) + else if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\r\nZREVRA"u8) && *(ulong*)(ptr + 11) == MemoryMarshal.Read("NGEBYLEX"u8) && *(ushort*)(ptr + 19) == CrLf) { return RespCommand.ZREVRANGEBYLEX; } break; case 15: - if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("\nZREMRAN"u8) && *(ulong*)(ptr + 12) == MemoryMarshal.Read("GEBYRANK"u8) && *(ushort*)(ptr + 20) == MemoryMarshal.Read("\r\n"u8)) + if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("\nZREMRAN"u8) && *(ulong*)(ptr + 12) == MemoryMarshal.Read("GEBYRANK"u8) && *(ushort*)(ptr + 20) == CrLf) { return RespCommand.ZREMRANGEBYRANK; } @@ -1769,21 +1729,29 @@ private bool TryParseCustomCommand(ReadOnlySpan command, out RespCommand c /// /// Parses the receive buffer, starting from the current read head, for all command names that are /// not covered by FastParseArrayCommand() and advances the read head to the end of the command name. - /// + /// /// NOTE: Assumes the input command names have already been converted to upper-case. /// /// Reference to the number of remaining tokens in the packet. Will be reduced to number of command arguments. /// If the command could not be parsed, will be non-empty if a specific error message should be returned. - /// True if the input RESP string was completely included in the buffer, false if we couldn't read the full command name. + /// True if the input RESP string was completely included in the buffer, false if we couldn't read the full command name. /// The parsed command name. - private RespCommand SlowParseCommand(ref int count, ref ReadOnlySpan specificErrorMsg, out bool success) + private RespCommand SlowParseCommand(ref int count, ref ReadOnlySpan specificErrorMsg, out bool commandReceived, + ReadOnlySpan command = default, ArgSlice subCommand = default) { - // Try to extract the current string from the front of the read head - var command = GetCommand(out success); + if (command.IsEmpty) + { + // Try to extract the current string from the front of the read head + command = GetCommand(out commandReceived); - if (!success) + if (!commandReceived) + { + return RespCommand.INVALID; + } + } + else { - return RespCommand.INVALID; + commandReceived = true; } // Account for the command name being taken off the read head @@ -1795,67 +1763,42 @@ private RespCommand SlowParseCommand(ref int count, ref ReadOnlySpan speci } else { - return SlowParseCommand(command, ref count, ref specificErrorMsg, out success); + return SlowParseCommand(command, ref count, ref specificErrorMsg, out commandReceived, subCommand); } } - private RespCommand SlowParseCommand(ReadOnlySpan command, ref int count, ref ReadOnlySpan specificErrorMsg, out bool success) + private RespCommand SlowParseCommand(ReadOnlySpan command, ref int count, + ref ReadOnlySpan specificErrorMsg, out bool commandReceived, + ArgSlice inlineSubCommand = default) { - success = true; - if (command.SequenceEqual(CmdStrings.SUBSCRIBE)) + commandReceived = true; + if (command.SequenceEqual(CmdStrings.HELLO)) { - return RespCommand.SUBSCRIBE; + return RespCommand.HELLO; } - else if (command.SequenceEqual(CmdStrings.SSUBSCRIBE)) + else if (command.SequenceEqual(CmdStrings.AUTH)) { - return RespCommand.SSUBSCRIBE; + return RespCommand.AUTH; } - else if (command.SequenceEqual(CmdStrings.RUNTXP)) + else if (command.SequenceEqual(CmdStrings.PING)) { - return RespCommand.RUNTXP; + return RespCommand.PING; } - else if (command.SequenceEqual(CmdStrings.SCRIPT)) + else if (command.SequenceEqual(CmdStrings.TIME)) { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.SCRIPT))); - return RespCommand.INVALID; - } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) - { - success = false; - return RespCommand.NONE; - } - - count--; - - if (subCommand.SequenceEqual(CmdStrings.LOAD)) - { - return RespCommand.SCRIPT_LOAD; - } - - if (subCommand.SequenceEqual(CmdStrings.FLUSH)) - { - return RespCommand.SCRIPT_FLUSH; - } - - if (subCommand.SequenceEqual(CmdStrings.EXISTS)) - { - return RespCommand.SCRIPT_EXISTS; - } - - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.SCRIPT)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; + return RespCommand.TIME; } - else if (command.SequenceEqual(CmdStrings.ECHO)) + else if (command.SequenceEqual(CmdStrings.QUIT)) { - return RespCommand.ECHO; + return RespCommand.QUIT; + } + else if (command.SequenceEqual(CmdStrings.SUBSCRIBE)) + { + return RespCommand.SUBSCRIBE; + } + else if (command.SequenceEqual(CmdStrings.SSUBSCRIBE)) + { + return RespCommand.SSUBSCRIBE; } else if (command.SequenceEqual(CmdStrings.GEORADIUS)) { @@ -1873,6 +1816,10 @@ private RespCommand SlowParseCommand(ReadOnlySpan command, ref int count, { return RespCommand.GEORADIUSBYMEMBER_RO; } + else if (command.SequenceEqual(CmdStrings.ECHO)) + { + return RespCommand.ECHO; + } else if (command.SequenceEqual(CmdStrings.REPLICAOF)) { return RespCommand.REPLICAOF; @@ -1881,701 +1828,678 @@ private RespCommand SlowParseCommand(ReadOnlySpan command, ref int count, { return RespCommand.SECONDARYOF; } - else if (command.SequenceEqual(CmdStrings.CONFIG)) + else if (command.SequenceEqual(CmdStrings.INFO)) { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.CONFIG))); - return RespCommand.INVALID; - } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) - { - success = false; - return RespCommand.NONE; - } - - count--; - - if (subCommand.SequenceEqual(CmdStrings.GET)) - { - return RespCommand.CONFIG_GET; - } - else if (subCommand.SequenceEqual(CmdStrings.REWRITE)) - { - return RespCommand.CONFIG_REWRITE; - } - else if (subCommand.SequenceEqual(CmdStrings.SET)) - { - return RespCommand.CONFIG_SET; - } - - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.CONFIG)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; + return RespCommand.INFO; } - else if (command.SequenceEqual(CmdStrings.CLIENT)) + else if (command.SequenceEqual(CmdStrings.ROLE)) { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.CLIENT))); - return RespCommand.INVALID; - } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) - { - success = false; - return RespCommand.NONE; - } - - count--; - - if (subCommand.SequenceEqual(CmdStrings.ID)) - { - return RespCommand.CLIENT_ID; - } - else if (subCommand.SequenceEqual(CmdStrings.INFO)) - { - return RespCommand.CLIENT_INFO; - } - else if (subCommand.SequenceEqual(CmdStrings.LIST)) - { - return RespCommand.CLIENT_LIST; - } - else if (subCommand.SequenceEqual(CmdStrings.KILL)) - { - return RespCommand.CLIENT_KILL; - } - else if (subCommand.SequenceEqual(CmdStrings.GETNAME)) - { - return RespCommand.CLIENT_GETNAME; - } - else if (subCommand.SequenceEqual(CmdStrings.SETNAME)) - { - return RespCommand.CLIENT_SETNAME; - } - else if (subCommand.SequenceEqual(CmdStrings.SETINFO)) - { - return RespCommand.CLIENT_SETINFO; - } - else if (subCommand.SequenceEqual(CmdStrings.UNBLOCK)) - { - return RespCommand.CLIENT_UNBLOCK; - } - - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.CLIENT)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; + return RespCommand.ROLE; } - else if (command.SequenceEqual(CmdStrings.AUTH)) + else if (command.SequenceEqual(CmdStrings.SAVE)) { - return RespCommand.AUTH; + return RespCommand.SAVE; } - else if (command.SequenceEqual(CmdStrings.INFO)) + else if (command.SequenceEqual(CmdStrings.RUNTXP)) { - return RespCommand.INFO; + return RespCommand.RUNTXP; } - else if (command.SequenceEqual(CmdStrings.ROLE)) + else if (command.SequenceEqual(CmdStrings.EXPDELSCAN)) { - return RespCommand.ROLE; + return RespCommand.EXPDELSCAN; } - else if (command.SequenceEqual(CmdStrings.COMMAND)) + else if (command.SequenceEqual(CmdStrings.LASTSAVE)) { - if (count == 0) - { - return RespCommand.COMMAND; - } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) - { - success = false; - return RespCommand.NONE; - } - - count--; - - if (subCommand.SequenceEqual(CmdStrings.COUNT)) - { - return RespCommand.COMMAND_COUNT; - } - - if (subCommand.SequenceEqual(CmdStrings.INFO)) - { - return RespCommand.COMMAND_INFO; - } - - if (subCommand.SequenceEqual(CmdStrings.DOCS)) - { - return RespCommand.COMMAND_DOCS; - } - - if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYS)) - { - return RespCommand.COMMAND_GETKEYS; - } - - if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYSANDFLAGS)) - { - return RespCommand.COMMAND_GETKEYSANDFLAGS; - } - - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.COMMAND)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; + return RespCommand.LASTSAVE; } - else if (command.SequenceEqual(CmdStrings.PING)) + else if (command.SequenceEqual(CmdStrings.BGSAVE)) { - return RespCommand.PING; + return RespCommand.BGSAVE; } - else if (command.SequenceEqual(CmdStrings.HELLO)) + else if (command.SequenceEqual(CmdStrings.COMMITAOF)) { - return RespCommand.HELLO; + return RespCommand.COMMITAOF; + } + else if (command.SequenceEqual(CmdStrings.FLUSHALL)) + { + return RespCommand.FLUSHALL; + } + else if (command.SequenceEqual(CmdStrings.FLUSHDB)) + { + return RespCommand.FLUSHDB; + } + else if (command.SequenceEqual(CmdStrings.FORCEGC)) + { + return RespCommand.FORCEGC; + } + else if (command.SequenceEqual(CmdStrings.MIGRATE)) + { + return RespCommand.MIGRATE; + } + else if (command.SequenceEqual(CmdStrings.PURGEBP)) + { + return RespCommand.PURGEBP; + } + else if (command.SequenceEqual(CmdStrings.FAILOVER)) + { + return RespCommand.FAILOVER; + } + else if (command.SequenceEqual(CmdStrings.MONITOR)) + { + return RespCommand.MONITOR; + } + else if (command.SequenceEqual(CmdStrings.REGISTERCS)) + { + return RespCommand.REGISTERCS; + } + else if (command.SequenceEqual(CmdStrings.ASYNC)) + { + return RespCommand.ASYNC; + } + else if (command.SequenceEqual(CmdStrings.HCOLLECT)) + { + return RespCommand.HCOLLECT; + } + else if (command.SequenceEqual(CmdStrings.DEBUG)) + { + return RespCommand.DEBUG; + } + else if (command.SequenceEqual(CmdStrings.ZCOLLECT)) + { + return RespCommand.ZCOLLECT; + } + // Note: The commands below are not slow path commands, so they should probably move to earlier. + else if (command.SequenceEqual(CmdStrings.SETIFMATCH)) + { + return RespCommand.SETIFMATCH; } - else if (command.SequenceEqual(CmdStrings.CLUSTER)) + else if (command.SequenceEqual(CmdStrings.SETIFGREATER)) + { + return RespCommand.SETIFGREATER; + } + else if (command.SequenceEqual(CmdStrings.GETWITHETAG)) + { + return RespCommand.GETWITHETAG; + } + else if (command.SequenceEqual(CmdStrings.GETIFNOTMATCH)) + { + return RespCommand.GETIFNOTMATCH; + } + else if (command.SequenceEqual(CmdStrings.DELIFGREATER)) + { + return RespCommand.DELIFGREATER; + } + else { - if (count == 0) + ReadOnlySpan subCommand = default; + var oldReadHead = -1; + + if (count > 0) { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.CLUSTER))); - return RespCommand.INVALID; + if (inlineSubCommand.length != 0) + { + AsciiUtils.ToUpperInPlace(inlineSubCommand.Span); + subCommand = inlineSubCommand.ReadOnlySpan; + } + else + { + oldReadHead = readHead; + // Optimistically advance readHead + subCommand = GetUpperCaseCommand(out var gotSubCommand); + if (!gotSubCommand) + { + commandReceived = false; + return RespCommand.NONE; + } + } } - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) + if (command.SequenceEqual(CmdStrings.CLIENT)) { - success = false; - return RespCommand.NONE; - } + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.CLIENT))); + return RespCommand.INVALID; + } - count--; + count--; - if (subCommand.SequenceEqual(CmdStrings.BUMPEPOCH)) - { - return RespCommand.CLUSTER_BUMPEPOCH; - } - else if (subCommand.SequenceEqual(CmdStrings.FORGET)) - { - return RespCommand.CLUSTER_FORGET; - } - else if (subCommand.SequenceEqual(CmdStrings.gossip)) - { - return RespCommand.CLUSTER_GOSSIP; - } - else if (subCommand.SequenceEqual(CmdStrings.INFO)) - { - return RespCommand.CLUSTER_INFO; - } - else if (subCommand.SequenceEqual(CmdStrings.MEET)) - { - return RespCommand.CLUSTER_MEET; - } - else if (subCommand.SequenceEqual(CmdStrings.MYID)) - { - return RespCommand.CLUSTER_MYID; - } - else if (subCommand.SequenceEqual(CmdStrings.myparentid)) - { - return RespCommand.CLUSTER_MYPARENTID; - } - else if (subCommand.SequenceEqual(CmdStrings.NODES)) - { - return RespCommand.CLUSTER_NODES; - } - else if (subCommand.SequenceEqual(CmdStrings.SHARDS)) - { - return RespCommand.CLUSTER_SHARDS; - } - else if (subCommand.SequenceEqual(CmdStrings.RESET)) - { - return RespCommand.CLUSTER_RESET; - } - else if (subCommand.SequenceEqual(CmdStrings.FAILOVER)) - { - return RespCommand.CLUSTER_FAILOVER; - } - else if (subCommand.SequenceEqual(CmdStrings.ADDSLOTS)) - { - return RespCommand.CLUSTER_ADDSLOTS; - } - else if (subCommand.SequenceEqual(CmdStrings.ADDSLOTSRANGE)) - { - return RespCommand.CLUSTER_ADDSLOTSRANGE; - } - else if (subCommand.SequenceEqual(CmdStrings.COUNTKEYSINSLOT)) - { - return RespCommand.CLUSTER_COUNTKEYSINSLOT; - } - else if (subCommand.SequenceEqual(CmdStrings.DELSLOTS)) - { - return RespCommand.CLUSTER_DELSLOTS; - } - else if (subCommand.SequenceEqual(CmdStrings.DELSLOTSRANGE)) - { - return RespCommand.CLUSTER_DELSLOTSRANGE; - } - else if (subCommand.SequenceEqual(CmdStrings.GETKEYSINSLOT)) - { - return RespCommand.CLUSTER_GETKEYSINSLOT; - } - else if (subCommand.SequenceEqual(CmdStrings.HELP)) - { - return RespCommand.CLUSTER_HELP; - } - else if (subCommand.SequenceEqual(CmdStrings.KEYSLOT)) - { - return RespCommand.CLUSTER_KEYSLOT; - } - else if (subCommand.SequenceEqual(CmdStrings.SETSLOT)) - { - return RespCommand.CLUSTER_SETSLOT; - } - else if (subCommand.SequenceEqual(CmdStrings.SLOTS)) - { - return RespCommand.CLUSTER_SLOTS; - } - else if (subCommand.SequenceEqual(CmdStrings.REPLICAS)) - { - return RespCommand.CLUSTER_REPLICAS; - } - else if (subCommand.SequenceEqual(CmdStrings.REPLICATE)) - { - return RespCommand.CLUSTER_REPLICATE; - } - else if (subCommand.SequenceEqual(CmdStrings.delkeysinslot)) - { - return RespCommand.CLUSTER_DELKEYSINSLOT; - } - else if (subCommand.SequenceEqual(CmdStrings.delkeysinslotrange)) - { - return RespCommand.CLUSTER_DELKEYSINSLOTRANGE; - } - else if (subCommand.SequenceEqual(CmdStrings.setslotsrange)) - { - return RespCommand.CLUSTER_SETSLOTSRANGE; - } - else if (subCommand.SequenceEqual(CmdStrings.slotstate)) - { - return RespCommand.CLUSTER_SLOTSTATE; - } - else if (subCommand.SequenceEqual(CmdStrings.publish)) - { - return RespCommand.CLUSTER_PUBLISH; - } - else if (subCommand.SequenceEqual(CmdStrings.spublish)) - { - return RespCommand.CLUSTER_SPUBLISH; - } - else if (subCommand.SequenceEqual(CmdStrings.MIGRATE)) - { - return RespCommand.CLUSTER_MIGRATE; - } - else if (subCommand.SequenceEqual(CmdStrings.mtasks)) - { - return RespCommand.CLUSTER_MTASKS; - } - else if (subCommand.SequenceEqual(CmdStrings.aofsync)) - { - return RespCommand.CLUSTER_AOFSYNC; - } - else if (subCommand.SequenceEqual(CmdStrings.appendlog)) - { - return RespCommand.CLUSTER_APPENDLOG; - } - else if (subCommand.SequenceEqual(CmdStrings.attach_sync)) - { - return RespCommand.CLUSTER_ATTACH_SYNC; - } - else if (subCommand.SequenceEqual(CmdStrings.banlist)) - { - return RespCommand.CLUSTER_BANLIST; - } - else if (subCommand.SequenceEqual(CmdStrings.begin_replica_recover)) - { - return RespCommand.CLUSTER_BEGIN_REPLICA_RECOVER; - } - else if (subCommand.SequenceEqual(CmdStrings.endpoint)) - { - return RespCommand.CLUSTER_ENDPOINT; - } - else if (subCommand.SequenceEqual(CmdStrings.failreplicationoffset)) - { - return RespCommand.CLUSTER_FAILREPLICATIONOFFSET; - } - else if (subCommand.SequenceEqual(CmdStrings.failstopwrites)) - { - return RespCommand.CLUSTER_FAILSTOPWRITES; - } - else if (subCommand.SequenceEqual(CmdStrings.FLUSHALL)) - { - return RespCommand.CLUSTER_FLUSHALL; - } - else if (subCommand.SequenceEqual(CmdStrings.SETCONFIGEPOCH)) - { - return RespCommand.CLUSTER_SETCONFIGEPOCH; - } - else if (subCommand.SequenceEqual(CmdStrings.initiate_replica_sync)) - { - return RespCommand.CLUSTER_INITIATE_REPLICA_SYNC; - } - else if (subCommand.SequenceEqual(CmdStrings.send_ckpt_file_segment)) - { - return RespCommand.CLUSTER_SEND_CKPT_FILE_SEGMENT; - } - else if (subCommand.SequenceEqual(CmdStrings.send_ckpt_metadata)) - { - return RespCommand.CLUSTER_SEND_CKPT_METADATA; - } - else if (subCommand.SequenceEqual(CmdStrings.cluster_sync)) - { - return RespCommand.CLUSTER_SYNC; - } + if (subCommand.SequenceEqual(CmdStrings.ID)) + { + return RespCommand.CLIENT_ID; + } + else if (subCommand.SequenceEqual(CmdStrings.INFO)) + { + return RespCommand.CLIENT_INFO; + } + else if (subCommand.SequenceEqual(CmdStrings.LIST)) + { + return RespCommand.CLIENT_LIST; + } + else if (subCommand.SequenceEqual(CmdStrings.KILL)) + { + return RespCommand.CLIENT_KILL; + } + else if (subCommand.SequenceEqual(CmdStrings.GETNAME)) + { + return RespCommand.CLIENT_GETNAME; + } + else if (subCommand.SequenceEqual(CmdStrings.SETNAME)) + { + return RespCommand.CLIENT_SETNAME; + } + else if (subCommand.SequenceEqual(CmdStrings.SETINFO)) + { + return RespCommand.CLIENT_SETINFO; + } + else if (subCommand.SequenceEqual(CmdStrings.UNBLOCK)) + { + return RespCommand.CLIENT_UNBLOCK; + } - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommand, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.CLUSTER)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; - } - else if (command.SequenceEqual(CmdStrings.LATENCY)) - { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.LATENCY))); + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.CLIENT)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); return RespCommand.INVALID; } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) + else if (command.SequenceEqual(CmdStrings.COMMAND)) { - success = false; - return RespCommand.NONE; - } + if (count == 0) + { + return RespCommand.COMMAND; + } - count--; + count--; - if (subCommand.SequenceEqual(CmdStrings.HELP)) - { - return RespCommand.LATENCY_HELP; - } - else if (subCommand.SequenceEqual(CmdStrings.HISTOGRAM)) - { - return RespCommand.LATENCY_HISTOGRAM; - } - else if (subCommand.SequenceEqual(CmdStrings.RESET)) - { - return RespCommand.LATENCY_RESET; - } + if (subCommand.SequenceEqual(CmdStrings.COUNT)) + { + return RespCommand.COMMAND_COUNT; + } - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommand, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.LATENCY)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; - } - else if (command.SequenceEqual(CmdStrings.SLOWLOG)) - { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.SLOWLOG))); + if (subCommand.SequenceEqual(CmdStrings.INFO)) + { + return RespCommand.COMMAND_INFO; + } + + if (subCommand.SequenceEqual(CmdStrings.DOCS)) + { + return RespCommand.COMMAND_DOCS; + } + + if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYS)) + { + return RespCommand.COMMAND_GETKEYS; + } + + if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYSANDFLAGS)) + { + return RespCommand.COMMAND_GETKEYSANDFLAGS; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.COMMAND)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); + return RespCommand.INVALID; } - else if (count >= 1) + else if (command.SequenceEqual(CmdStrings.CLUSTER)) { - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) + if (count == 0) { - success = false; - return RespCommand.NONE; + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.CLUSTER))); + return RespCommand.INVALID; } count--; - if (subCommand.SequenceEqual(CmdStrings.HELP)) + if (subCommand.SequenceEqual(CmdStrings.BUMPEPOCH)) { - return RespCommand.SLOWLOG_HELP; + return RespCommand.CLUSTER_BUMPEPOCH; } - else if (subCommand.SequenceEqual(CmdStrings.GET)) + else if (subCommand.SequenceEqual(CmdStrings.FORGET)) { - return RespCommand.SLOWLOG_GET; + return RespCommand.CLUSTER_FORGET; } - else if (subCommand.SequenceEqual(CmdStrings.LEN)) + else if (subCommand.SequenceEqual(CmdStrings.gossip)) { - return RespCommand.SLOWLOG_LEN; + return RespCommand.CLUSTER_GOSSIP; + } + else if (subCommand.SequenceEqual(CmdStrings.INFO)) + { + return RespCommand.CLUSTER_INFO; + } + else if (subCommand.SequenceEqual(CmdStrings.MEET)) + { + return RespCommand.CLUSTER_MEET; + } + else if (subCommand.SequenceEqual(CmdStrings.MYID)) + { + return RespCommand.CLUSTER_MYID; + } + else if (subCommand.SequenceEqual(CmdStrings.myparentid)) + { + return RespCommand.CLUSTER_MYPARENTID; + } + else if (subCommand.SequenceEqual(CmdStrings.NODES)) + { + return RespCommand.CLUSTER_NODES; + } + else if (subCommand.SequenceEqual(CmdStrings.SHARDS)) + { + return RespCommand.CLUSTER_SHARDS; } else if (subCommand.SequenceEqual(CmdStrings.RESET)) { - return RespCommand.SLOWLOG_RESET; + return RespCommand.CLUSTER_RESET; } - } - } - else if (command.SequenceEqual(CmdStrings.TIME)) - { - return RespCommand.TIME; - } - else if (command.SequenceEqual(CmdStrings.QUIT)) - { - return RespCommand.QUIT; - } - else if (command.SequenceEqual(CmdStrings.SAVE)) - { - return RespCommand.SAVE; - } - else if (command.SequenceEqual(CmdStrings.EXPDELSCAN)) - { - return RespCommand.EXPDELSCAN; - } - else if (command.SequenceEqual(CmdStrings.LASTSAVE)) - { - return RespCommand.LASTSAVE; - } - else if (command.SequenceEqual(CmdStrings.BGSAVE)) - { - return RespCommand.BGSAVE; - } - else if (command.SequenceEqual(CmdStrings.COMMITAOF)) - { - return RespCommand.COMMITAOF; - } - else if (command.SequenceEqual(CmdStrings.FLUSHALL)) - { - return RespCommand.FLUSHALL; - } - else if (command.SequenceEqual(CmdStrings.FLUSHDB)) - { - return RespCommand.FLUSHDB; - } - else if (command.SequenceEqual(CmdStrings.FORCEGC)) - { - return RespCommand.FORCEGC; - } - else if (command.SequenceEqual(CmdStrings.MIGRATE)) - { - return RespCommand.MIGRATE; - } - else if (command.SequenceEqual(CmdStrings.PURGEBP)) - { - return RespCommand.PURGEBP; - } - else if (command.SequenceEqual(CmdStrings.FAILOVER)) - { - return RespCommand.FAILOVER; - } - else if (command.SequenceEqual(CmdStrings.MEMORY)) - { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.MEMORY))); + else if (subCommand.SequenceEqual(CmdStrings.FAILOVER)) + { + return RespCommand.CLUSTER_FAILOVER; + } + else if (subCommand.SequenceEqual(CmdStrings.ADDSLOTS)) + { + return RespCommand.CLUSTER_ADDSLOTS; + } + else if (subCommand.SequenceEqual(CmdStrings.ADDSLOTSRANGE)) + { + return RespCommand.CLUSTER_ADDSLOTSRANGE; + } + else if (subCommand.SequenceEqual(CmdStrings.COUNTKEYSINSLOT)) + { + return RespCommand.CLUSTER_COUNTKEYSINSLOT; + } + else if (subCommand.SequenceEqual(CmdStrings.DELSLOTS)) + { + return RespCommand.CLUSTER_DELSLOTS; + } + else if (subCommand.SequenceEqual(CmdStrings.DELSLOTSRANGE)) + { + return RespCommand.CLUSTER_DELSLOTSRANGE; + } + else if (subCommand.SequenceEqual(CmdStrings.GETKEYSINSLOT)) + { + return RespCommand.CLUSTER_GETKEYSINSLOT; + } + else if (subCommand.SequenceEqual(CmdStrings.HELP)) + { + return RespCommand.CLUSTER_HELP; + } + else if (subCommand.SequenceEqual(CmdStrings.KEYSLOT)) + { + return RespCommand.CLUSTER_KEYSLOT; + } + else if (subCommand.SequenceEqual(CmdStrings.SETSLOT)) + { + return RespCommand.CLUSTER_SETSLOT; + } + else if (subCommand.SequenceEqual(CmdStrings.SLOTS)) + { + return RespCommand.CLUSTER_SLOTS; + } + else if (subCommand.SequenceEqual(CmdStrings.REPLICAS)) + { + return RespCommand.CLUSTER_REPLICAS; + } + else if (subCommand.SequenceEqual(CmdStrings.REPLICATE)) + { + return RespCommand.CLUSTER_REPLICATE; + } + else if (subCommand.SequenceEqual(CmdStrings.delkeysinslot)) + { + return RespCommand.CLUSTER_DELKEYSINSLOT; + } + else if (subCommand.SequenceEqual(CmdStrings.delkeysinslotrange)) + { + return RespCommand.CLUSTER_DELKEYSINSLOTRANGE; + } + else if (subCommand.SequenceEqual(CmdStrings.setslotsrange)) + { + return RespCommand.CLUSTER_SETSLOTSRANGE; + } + else if (subCommand.SequenceEqual(CmdStrings.slotstate)) + { + return RespCommand.CLUSTER_SLOTSTATE; + } + else if (subCommand.SequenceEqual(CmdStrings.publish)) + { + return RespCommand.CLUSTER_PUBLISH; + } + else if (subCommand.SequenceEqual(CmdStrings.spublish)) + { + return RespCommand.CLUSTER_SPUBLISH; + } + else if (subCommand.SequenceEqual(CmdStrings.MIGRATE)) + { + return RespCommand.CLUSTER_MIGRATE; + } + else if (subCommand.SequenceEqual(CmdStrings.mtasks)) + { + return RespCommand.CLUSTER_MTASKS; + } + else if (subCommand.SequenceEqual(CmdStrings.aofsync)) + { + return RespCommand.CLUSTER_AOFSYNC; + } + else if (subCommand.SequenceEqual(CmdStrings.appendlog)) + { + return RespCommand.CLUSTER_APPENDLOG; + } + else if (subCommand.SequenceEqual(CmdStrings.attach_sync)) + { + return RespCommand.CLUSTER_ATTACH_SYNC; + } + else if (subCommand.SequenceEqual(CmdStrings.banlist)) + { + return RespCommand.CLUSTER_BANLIST; + } + else if (subCommand.SequenceEqual(CmdStrings.begin_replica_recover)) + { + return RespCommand.CLUSTER_BEGIN_REPLICA_RECOVER; + } + else if (subCommand.SequenceEqual(CmdStrings.endpoint)) + { + return RespCommand.CLUSTER_ENDPOINT; + } + else if (subCommand.SequenceEqual(CmdStrings.failreplicationoffset)) + { + return RespCommand.CLUSTER_FAILREPLICATIONOFFSET; + } + else if (subCommand.SequenceEqual(CmdStrings.failstopwrites)) + { + return RespCommand.CLUSTER_FAILSTOPWRITES; + } + else if (subCommand.SequenceEqual(CmdStrings.FLUSHALL)) + { + return RespCommand.CLUSTER_FLUSHALL; + } + else if (subCommand.SequenceEqual(CmdStrings.SETCONFIGEPOCH)) + { + return RespCommand.CLUSTER_SETCONFIGEPOCH; + } + else if (subCommand.SequenceEqual(CmdStrings.initiate_replica_sync)) + { + return RespCommand.CLUSTER_INITIATE_REPLICA_SYNC; + } + else if (subCommand.SequenceEqual(CmdStrings.send_ckpt_file_segment)) + { + return RespCommand.CLUSTER_SEND_CKPT_FILE_SEGMENT; + } + else if (subCommand.SequenceEqual(CmdStrings.send_ckpt_metadata)) + { + return RespCommand.CLUSTER_SEND_CKPT_METADATA; + } + else if (subCommand.SequenceEqual(CmdStrings.cluster_sync)) + { + return RespCommand.CLUSTER_SYNC; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommand, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.CLUSTER)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); return RespCommand.INVALID; } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) + else if (command.SequenceEqual(CmdStrings.SCRIPT)) { - success = false; - return RespCommand.NONE; - } + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.SCRIPT))); + return RespCommand.INVALID; + } - count--; + count--; - if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.USAGE)) - { - return RespCommand.MEMORY_USAGE; - } + if (subCommand.SequenceEqual(CmdStrings.LOAD)) + { + return RespCommand.SCRIPT_LOAD; + } - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.MEMORY)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; - } - else if (command.SequenceEqual(CmdStrings.MONITOR)) - { - return RespCommand.MONITOR; - } - else if (command.SequenceEqual(CmdStrings.ACL)) - { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.ACL))); + if (subCommand.SequenceEqual(CmdStrings.FLUSH)) + { + return RespCommand.SCRIPT_FLUSH; + } + + if (subCommand.SequenceEqual(CmdStrings.EXISTS)) + { + return RespCommand.SCRIPT_EXISTS; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.SCRIPT)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); return RespCommand.INVALID; } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) + else if (command.SequenceEqual(CmdStrings.CONFIG)) { - success = false; - return RespCommand.NONE; - } + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.CONFIG))); + return RespCommand.INVALID; + } - count--; + count--; - if (subCommand.SequenceEqual(CmdStrings.CAT)) - { - return RespCommand.ACL_CAT; - } - else if (subCommand.SequenceEqual(CmdStrings.DELUSER)) - { - return RespCommand.ACL_DELUSER; - } - else if (subCommand.SequenceEqual(CmdStrings.GENPASS)) - { - return RespCommand.ACL_GENPASS; - } - else if (subCommand.SequenceEqual(CmdStrings.GETUSER)) - { - return RespCommand.ACL_GETUSER; - } - else if (subCommand.SequenceEqual(CmdStrings.LIST)) - { - return RespCommand.ACL_LIST; - } - else if (subCommand.SequenceEqual(CmdStrings.LOAD)) - { - return RespCommand.ACL_LOAD; + if (subCommand.SequenceEqual(CmdStrings.GET)) + { + return RespCommand.CONFIG_GET; + } + else if (subCommand.SequenceEqual(CmdStrings.REWRITE)) + { + return RespCommand.CONFIG_REWRITE; + } + else if (subCommand.SequenceEqual(CmdStrings.SET)) + { + return RespCommand.CONFIG_SET; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.CONFIG)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); + return RespCommand.INVALID; } - else if (subCommand.SequenceEqual(CmdStrings.SAVE)) + else if (command.SequenceEqual(CmdStrings.PUBSUB)) { - return RespCommand.ACL_SAVE; + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.PUBSUB))); + return RespCommand.INVALID; + } + + count--; + + if (subCommand.SequenceEqual(CmdStrings.CHANNELS)) + { + return RespCommand.PUBSUB_CHANNELS; + } + else if (subCommand.SequenceEqual(CmdStrings.NUMSUB)) + { + return RespCommand.PUBSUB_NUMSUB; + } + else if (subCommand.SequenceEqual(CmdStrings.NUMPAT)) + { + return RespCommand.PUBSUB_NUMPAT; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.PUBSUB)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); + return RespCommand.INVALID; } - else if (subCommand.SequenceEqual(CmdStrings.SETUSER)) + else if (command.SequenceEqual(CmdStrings.ACL)) { - return RespCommand.ACL_SETUSER; + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.ACL))); + return RespCommand.INVALID; + } + + count--; + + if (subCommand.SequenceEqual(CmdStrings.CAT)) + { + return RespCommand.ACL_CAT; + } + else if (subCommand.SequenceEqual(CmdStrings.DELUSER)) + { + return RespCommand.ACL_DELUSER; + } + else if (subCommand.SequenceEqual(CmdStrings.GENPASS)) + { + return RespCommand.ACL_GENPASS; + } + else if (subCommand.SequenceEqual(CmdStrings.GETUSER)) + { + return RespCommand.ACL_GETUSER; + } + else if (subCommand.SequenceEqual(CmdStrings.LIST)) + { + return RespCommand.ACL_LIST; + } + else if (subCommand.SequenceEqual(CmdStrings.LOAD)) + { + return RespCommand.ACL_LOAD; + } + else if (subCommand.SequenceEqual(CmdStrings.SAVE)) + { + return RespCommand.ACL_SAVE; + } + else if (subCommand.SequenceEqual(CmdStrings.SETUSER)) + { + return RespCommand.ACL_SETUSER; + } + else if (subCommand.SequenceEqual(CmdStrings.USERS)) + { + return RespCommand.ACL_USERS; + } + else if (subCommand.SequenceEqual(CmdStrings.WHOAMI)) + { + return RespCommand.ACL_WHOAMI; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.ACL)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); + return RespCommand.INVALID; } - else if (subCommand.SequenceEqual(CmdStrings.USERS)) + else if (command.SequenceEqual(CmdStrings.LATENCY)) { - return RespCommand.ACL_USERS; + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.LATENCY))); + return RespCommand.INVALID; + } + + count--; + + if (subCommand.SequenceEqual(CmdStrings.HELP)) + { + return RespCommand.LATENCY_HELP; + } + else if (subCommand.SequenceEqual(CmdStrings.HISTOGRAM)) + { + return RespCommand.LATENCY_HISTOGRAM; + } + else if (subCommand.SequenceEqual(CmdStrings.RESET)) + { + return RespCommand.LATENCY_RESET; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommand, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.LATENCY)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); + return RespCommand.INVALID; } - else if (subCommand.SequenceEqual(CmdStrings.WHOAMI)) + else if (command.SequenceEqual(CmdStrings.SLOWLOG)) { - return RespCommand.ACL_WHOAMI; - } + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.SLOWLOG))); + return RespCommand.INVALID; + } - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.ACL)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; - } - else if (command.SequenceEqual(CmdStrings.REGISTERCS)) - { - return RespCommand.REGISTERCS; - } - else if (command.SequenceEqual(CmdStrings.ASYNC)) - { - return RespCommand.ASYNC; - } - else if (command.SequenceEqual(CmdStrings.MODULE)) - { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.MODULE))); - return RespCommand.INVALID; + count--; + + if (subCommand.SequenceEqual(CmdStrings.HELP)) + { + return RespCommand.SLOWLOG_HELP; + } + else if (subCommand.SequenceEqual(CmdStrings.GET)) + { + return RespCommand.SLOWLOG_GET; + } + else if (subCommand.SequenceEqual(CmdStrings.LEN)) + { + return RespCommand.SLOWLOG_LEN; + } + else if (subCommand.SequenceEqual(CmdStrings.RESET)) + { + return RespCommand.SLOWLOG_RESET; + } } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) + else if (command.SequenceEqual(CmdStrings.MEMORY)) { - success = false; - return RespCommand.NONE; - } + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.MEMORY))); + return RespCommand.INVALID; + } - count--; + count--; - if (subCommand.SequenceEqual(CmdStrings.LOADCS)) - { - return RespCommand.MODULE_LOADCS; - } + if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.USAGE)) + { + return RespCommand.MEMORY_USAGE; + } - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.MODULE)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; - } - else if (command.SequenceEqual(CmdStrings.PUBSUB)) - { - if (count == 0) - { - specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, - nameof(RespCommand.PUBSUB))); + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.MEMORY)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); return RespCommand.INVALID; } - - var subCommand = GetUpperCaseCommand(out var gotSubCommand); - if (!gotSubCommand) + else if (command.SequenceEqual(CmdStrings.MODULE)) { - success = false; - return RespCommand.NONE; - } + if (count == 0) + { + specificErrorMsg = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, + nameof(RespCommand.MODULE))); + return RespCommand.INVALID; + } - count--; + count--; - if (subCommand.SequenceEqual(CmdStrings.CHANNELS)) - { - return RespCommand.PUBSUB_CHANNELS; - } - else if (subCommand.SequenceEqual(CmdStrings.NUMSUB)) - { - return RespCommand.PUBSUB_NUMSUB; + if (subCommand.SequenceEqual(CmdStrings.LOADCS)) + { + return RespCommand.MODULE_LOADCS; + } + + var errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, + Encoding.UTF8.GetString(subCommand), + nameof(RespCommand.MODULE)); + specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); + return RespCommand.INVALID; } - else if (subCommand.SequenceEqual(CmdStrings.NUMPAT)) + + // Reset read head if we didn't match command. + if (oldReadHead != -1) { - return RespCommand.PUBSUB_NUMPAT; + readHead = oldReadHead; } - - string errMsg = string.Format(CmdStrings.GenericErrUnknownSubCommandNoHelp, - Encoding.UTF8.GetString(subCommand), - nameof(RespCommand.PUBSUB)); - specificErrorMsg = Encoding.UTF8.GetBytes(errMsg); - return RespCommand.INVALID; - } - else if (command.SequenceEqual(CmdStrings.HCOLLECT)) - { - return RespCommand.HCOLLECT; - } - else if (command.SequenceEqual(CmdStrings.DEBUG)) - { - return RespCommand.DEBUG; - } - else if (command.SequenceEqual(CmdStrings.ZCOLLECT)) - { - return RespCommand.ZCOLLECT; - } - // Note: The commands below are not slow path commands, so they should probably move to earlier. - else if (command.SequenceEqual(CmdStrings.SETIFMATCH)) - { - return RespCommand.SETIFMATCH; - } - else if (command.SequenceEqual(CmdStrings.SETIFGREATER)) - { - return RespCommand.SETIFGREATER; - } - else if (command.SequenceEqual(CmdStrings.GETWITHETAG)) - { - return RespCommand.GETWITHETAG; - } - else if (command.SequenceEqual(CmdStrings.GETIFNOTMATCH)) - { - return RespCommand.GETIFNOTMATCH; - } - else if (command.SequenceEqual(CmdStrings.DELIFGREATER)) - { - return RespCommand.DELIFGREATER; } // If this command name was not known to the slow pass, we are out of options and the command is unknown. @@ -2591,7 +2515,7 @@ private bool AttemptSkipLine() // We might have received an inline command package.Try to find the end of the line. logger?.LogWarning("Received malformed input message. Trying to skip line."); - for (int stringEnd = readHead; stringEnd < bytesRead - 1; stringEnd++) + for (var stringEnd = readHead; stringEnd < bytesRead - 1; stringEnd++) { if (recvBufferPtr[stringEnd] == '\r' && recvBufferPtr[stringEnd + 1] == '\n') { @@ -2607,7 +2531,7 @@ private bool AttemptSkipLine() /// /// Try to parse a command out of a provided buffer. - /// + /// /// Useful for when we have a command to validate somewhere, but aren't actually running it. /// internal RespCommand ParseRespCommandBuffer(ReadOnlySpan buffer) @@ -2637,9 +2561,9 @@ internal RespCommand ParseRespCommandBuffer(ReadOnlySpan buffer) /// /// Version of for fuzzing. - /// + /// /// Expects (and allows) partial commands. - /// + /// /// Returns true if a command was succesfully parsed /// internal bool FuzzParseCommandBuffer(ReadOnlySpan buffer, out RespCommand cmd) @@ -2699,40 +2623,73 @@ internal bool FuzzParseCommandBuffer(ReadOnlySpan buffer, out RespCommand /// Parses the command from the given input buffer. /// /// If true, when a parsing error occurs an error response will written. - /// Whether processing should continue or a parsing error occurred (e.g. out of tokens). + /// Whether processing should continue or a parsing error occurred (e.g. out of tokens). /// Command parsed from the input buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private RespCommand ParseCommand(bool writeErrorOnFailure, out bool success) + private RespCommand ParseCommand(bool writeErrorOnFailure, out bool commandReceived) { - RespCommand cmd = RespCommand.INVALID; + RespCommand cmd; // Initialize count as -1 (i.e., read head has not been advanced) - int count = -1; - success = true; + var count = -1; + commandReceived = true; endReadHead = readHead; + var ptr = recvBufferPtr + readHead; + var end = recvBufferPtr + bytesRead; + var remainingBytes = bytesRead - readHead; + // Attempt parsing using fast parse pass for most common operations - cmd = FastParseCommand(out count); + cmd = FastParseCommand(out count, ptr, remainingBytes); - // If we have not found a command, continue parsing on slow path if (cmd == RespCommand.NONE) { - cmd = ArrayParseCommand(writeErrorOnFailure, ref count, ref success); - if (!success) return cmd; + // See if input command is all upper-case. If not, convert and try fast parse pass again. + if (MakeUpperCase(ptr, remainingBytes)) + { + cmd = FastParseCommand(out count, ptr, remainingBytes); + } + + // If we have not found a command, continue parsing on slow path. + // But first ensure we are attempting to read a RESP array header. + if ((cmd == RespCommand.NONE) && (*ptr == '*')) + { + cmd = ArrayParseCommand(writeErrorOnFailure, ptr, end, out count, out commandReceived); + if (!commandReceived) return cmd; + } } - // Set up parse state - parseState.Initialize(count); - var ptr = recvBufferPtr + readHead; - for (int i = 0; i < count; i++) + if (cmd != RespCommand.NONE) { - if (!parseState.Read(i, ref ptr, recvBufferPtr + bytesRead)) + // Set up read pointer past the command. + ptr = recvBufferPtr + readHead; + + // Set up parse state + parseState.Initialize(count); + for (var i = 0; i < count; i++) { - success = false; - return RespCommand.INVALID; + if (!parseState.Read(i, ref ptr, end)) + { + commandReceived = false; + return RespCommand.INVALID; + } } + + readHead = (int)(ptr - recvBufferPtr); + } + else if (CanRunInlineCommands() && (*ptr != '*')) + { + cmd = TryParseInlineCommandline(writeErrorOnFailure, ref ptr, out commandReceived, ref count); + if (!commandReceived) + return RespCommand.INVALID; + } + else + { + commandReceived = AttemptSkipLine(); + return RespCommand.INVALID; } - endReadHead = (int)(ptr - recvBufferPtr); + + endReadHead = readHead; if (storeWrapper.serverOptions.EnableAOF && storeWrapper.serverOptions.WaitForCommit) HandleAofCommitMode(cmd); @@ -2740,6 +2697,365 @@ private RespCommand ParseCommand(bool writeErrorOnFailure, out bool success) return cmd; } + private RespCommand TryParseInlineCommandline(bool writeErrorOnFailure, ref byte* ptr, + out bool commandReceived, ref int count) + { + // This may be an inline command string. We'll parse it and convert a part to a RESP command string, + // which is then parsed to get the command. + SpanByteAndMemory spam = new(null); + if (!ParseInlineCommandline(ref spam, writeErrorOnFailure, ref ptr, out commandReceived, out var nbytes, out var result)) + { + if (commandReceived) + endReadHead = readHead; + return RespCommand.INVALID; + } + + // If we're here, commandReceived is true, and we've reached end-of-line. + endReadHead = readHead; + + fixed (byte* nptr = spam.Memory.Memory.Span) + { + var nend = nptr + nbytes; + + var cmd = FastParseCommand(out count, nptr, nbytes); + + // Since we're operating on a temporary buffer and not the actual receive buffer, + // we don't care for its commandReceived, and we reset readHead afterwards. + if (cmd == RespCommand.NONE) + { + cmd = ArrayParseCommand(writeErrorOnFailure, nptr, nend, out count, out _, + result[0].ReadOnlySpan, result.Length > 1 ? result[1] : default); + } + readHead = endReadHead; + + if (cmd == RespCommand.INVALID) + logger?.LogWarning("Received malformed input message. Line is skipped."); + else + // Note that arguments are initialized from the actual command string, and not our made-up RESP string. + parseState.InitializeWithArguments(result[(result.Length - count)..]); + + return cmd; + } + } + + private bool ParseInlineCommandline(ref SpanByteAndMemory spam, + bool writeErrorOnFailure, ref byte* ptr, out bool commandReceived, + out int nbytes, out ArgSlice[] result) + { + nbytes = 0; + result = default; + + // Minimum processing length is 4. But a user can send shorter lines + // (e.g. just pressing enter in telnet), and these get appended to next + // lines in inline mode. + // + // Another complication is that the terminator is different: + // It's CRLF in Normal RESP processing, but inline can also handle just LF. + // Also, A RESP packet can be sent in parts or be malformed, so + // it's possible this path tries to inline parse a partial RESP packet. + // + // In short: The way we process RESP is a bit hostile to inline processing, + // however normal RESP processing is prioritzed, so we'll have to work around it. + // + // First, we can exploit the fact that any such short command would be invalid. + // Every CRLF below the minimum can be discarded. + // Second, we can only check for CRLF and not LF, since discarding + // upto LF might conflict with partial normal RESP packets. + var tptr = ptr; + for (var i = 0; i < MinimumProcessLength - 1; ++i) + { + if (*(ushort*)tptr++ == CrLf) + { + ptr = tptr + 1; + + if (bytesRead - i < MinimumProcessLength) + { + commandReceived = true; + readHead = i + 2; + if (writeErrorOnFailure) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref dcurr, dend)) + SendAndReset(); + } + return false; + } + break; + } + } + + // We might have received an inline command package. Try parsing it. + if (!TryParseInlineCommandArguments(writeErrorOnFailure, out commandReceived, out result, + ref ptr, recvBufferPtr + bytesRead, out var error) || (result.Length == 0)) + { + if (!error.IsEmpty) + { + while (!RespWriteUtils.TryWriteError(error, ref dcurr, dend)) + SendAndReset(); + } + + // Move readHead to end of line in input package if we can + if (commandReceived) + { + logger?.LogWarning("Received malformed input message. Line is skipped."); + readHead = (int)(ptr - recvBufferPtr); + } + + // The second condition indicates line is CRLF. + return false; + } + + // Move readHead to end of line in input package + // Note there's no situation where we're here and commandReceived is false. + readHead = (int)(ptr - recvBufferPtr); + + // command matching only needs the first two elements. + var command = result[0]; + var subCommand = result.Length > 1 ? result[1] : default; + + // Make sure the command is uppercased. + MakeUpperCase(command.ptr, command.length); + + // We'll parse the result by creating a RESP string to parse and then calling the regular code. + + // Minumum estimate is array header + length + command + crlf + length + subcommand + crlf + nbytes = 4 + 4 + command.Length + 2 + 4 + subCommand.Length + 2; + + using var writer = new RespMemoryWriter(respProtocolVersion, ref spam); + + writer.Realloc(nbytes); + // We use the actual length because it may be read back later. + writer.WriteArrayLength(result.Length); + + // The resp string is technically invalid since the length doesn't match the actual items. + writer.WriteBulkString(command.ReadOnlySpan); + if (result.Length > 1) + writer.WriteBulkString(subCommand.ReadOnlySpan); + + nbytes = writer.GetPosition(); + return true; + } + + private static bool TryParseInlineCommandArguments(bool writeErrorOnFailure, out bool commandReceived, + out ArgSlice[] result, ref byte* ptr, byte* end, + out ReadOnlySpan error) + { + // The inline format is not defined beyond: + // "space-separated arguments in a telnet session" and + // "no command starts with * (the identifying byte of RESP Arrays)". + // + // Different reference versions do it differently, newer ones use quoting but without defining it anywhere. + // The behaviour implemented here is a slight superset of observed reference behaviour. + // + // We'll use consistent rules: + // A character or group of characters under the same rules is a sequence. + // There are four types of sequences: quoting, separators, escapes, or normal. + // There are two types of contexts: a quote context or a normal context. + // Quote contexts allow escaping depending on their starting character. + // + // Separators separate different arguments. + // Separators are start of line (implied), linefeed which marks end of line, and the characters: + // space, tab and carriage return, when they are in a normal context. + // In a quote context the same seqeunces are considered normal. + // + // Quote characters are ' or ". + // + // A quoting context starts with a quote character. + // A quoting context ends with the same quote character that followed by a separator, + // unless the quote character is escaped (see below). + // [References seem to not have the 'followed by a separator' rule, but just error out] + // + // A quote character not starting or ending a quote context is considered a normal character. + // + // The character \ inside a quote context starts an escape sequence. + // In quote contexts starting with " character, the escape sequence is the typical C escapes. + // Other characters are escaped to themselves. All resulting escaped characters are considered normal characters. + // In quote contexts starting with ' character, the only allowed escape is \' -> '. + // + // Normal sequences (or sequences considered such) are echoed to the output, everything else is not echoed. + // + + error = default; + result = default; + commandReceived = false; + + var slices = new System.Collections.Generic.List(); + var slicePtr = ptr; + + // True if the current slice has any contents. + var anyContents = false; + // True if any quote is used. If no quoting is used, we can save on unescaping later. + var anyQuote = false; + // Current quote char if any. + byte quoteChar = 0; + + while (ptr < end) + { + if (*ptr == '\n') + { + commandReceived = true; + if (anyContents) + { + slices.Add(new ArgSlice(slicePtr, (int)(ptr - slicePtr))); + } + + // Advance past newline + ptr++; + + if (quoteChar != 0) + { + if (writeErrorOnFailure) + { + error = CmdStrings.RESP_ERR_UNBALANCED_QUOTES; + } + return false; + } + + if (anyQuote && slices.Count > 0) + { + result = new ArgSlice[slices.Count]; + + // MakeUpperCase could have been run on the first slice earlier. + result[0] = ArgSliceUtils.Unescape(slices[0], true); + for (var i = 1; i < slices.Count; ++i) + { + result[i] = ArgSliceUtils.Unescape(slices[i]); + } + } + // If there are no quotes, we can be faster + else + { + result = [.. slices]; + } + + return true; + } + + if (AsciiUtils.IsQuoteChar(*ptr)) + { + if (quoteChar == 0) + { + quoteChar = *ptr; + anyContents = true; + anyQuote = true; + } + else if ((quoteChar == *ptr) && (ptr < end)) + { + var next = ptr + 1; + if (AsciiUtils.IsRedisWhiteSpace(*next) || (*next == '\n')) + { + var unQuote = true; + + // We do the unescaping separately, so we need an extra check here to see this quote isn't escaped. + if (quoteChar == '"') + { + for (var index = ptr - 1; index >= slicePtr && *index == '\\'; --index) + { + unQuote = !unQuote; + } + } + // We likely don't need this check, because to get quoteChar != 0 we needed to open up a quote earlier, + // which means slicePtr must have been earlier. But it costs little to be safer. + else if (ptr > slicePtr) + { + unQuote = *(ptr - 1) != '\\'; + } + + if (unQuote) + quoteChar = 0; + } + } + } + else if ((quoteChar == 0) && AsciiUtils.IsRedisWhiteSpace(*ptr)) + { + if (anyContents) + { + slices.Add(new ArgSlice(slicePtr, (int)(ptr - slicePtr))); + anyContents = false; + } + + slicePtr = ptr + 1; + } + else + { + anyContents = true; + } + + ++ptr; + } + + return false; + } + + + ReadOnlySpan GetCommand(out bool success) + { + var ptr = recvBufferPtr + readHead; + var end = recvBufferPtr + bytesRead; + + // Try the command length + if (!RespReadUtils.TryReadUnsignedLengthHeader(out int length, ref ptr, end)) + { + success = false; + return default; + } + + readHead = (int)(ptr - recvBufferPtr); + + // Try to read the command value + ptr += length; + if (ptr + 2 > end) + { + success = false; + return default; + } + + if (*(ushort*)ptr != CrLf) + { + RespParsingException.ThrowUnexpectedToken(*ptr); + } + + var result = new ReadOnlySpan(recvBufferPtr + readHead, length); + readHead += length + 2; + success = true; + + return result; + } + + ReadOnlySpan GetUpperCaseCommand(out bool success) + { + var ptr = recvBufferPtr + readHead; + var end = recvBufferPtr + bytesRead; + + // Try the command length + if (!RespReadUtils.TryReadUnsignedLengthHeader(out int length, ref ptr, end)) + { + success = false; + return default; + } + + readHead = (int)(ptr - recvBufferPtr); + + // Try to read the command value + ptr += length; + if (ptr + 2 > end) + { + success = false; + return default; + } + + if (*(ushort*)ptr != CrLf) + { + RespParsingException.ThrowUnexpectedToken(*ptr); + } + + var result = new Span(recvBufferPtr + readHead, length); + readHead += length + 2; + success = true; + + AsciiUtils.ToUpperInPlace(result); + return result; + } + [MethodImpl(MethodImplOptions.NoInlining)] private void HandleAofCommitMode(RespCommand cmd) { @@ -2751,7 +3067,7 @@ private void HandleAofCommitMode(RespCommand cmd) if (txnManager.state == TxnState.Started) return; - /* + /* If a previous command marked AOF for blocking we should not change AOF blocking flag. If no previous command marked AOF for blocking, then we only change AOF flag to block if the current command is AOF dependent. @@ -2760,51 +3076,37 @@ private void HandleAofCommitMode(RespCommand cmd) } [MethodImpl(MethodImplOptions.NoInlining)] - private RespCommand ArrayParseCommand(bool writeErrorOnFailure, ref int count, ref bool success) + private RespCommand ArrayParseCommand(bool writeErrorOnFailure, byte* ptr, byte* end, + out int count, out bool commandReceived, + ReadOnlySpan command = default, + ArgSlice subCommand = default) { RespCommand cmd = RespCommand.INVALID; ReadOnlySpan specificErrorMessage = default; + commandReceived = true; endReadHead = readHead; - var ptr = recvBufferPtr + readHead; - - // See if input command is all upper-case. If not, convert and try fast parse pass again. - if (MakeUpperCase(ptr, bytesRead - readHead)) - { - cmd = FastParseCommand(out count); - if (cmd != RespCommand.NONE) - { - return cmd; - } - } - - // Ensure we are attempting to read a RESP array header - if (recvBufferPtr[readHead] != '*') - { - // We might have received an inline command package. Skip until the end of the line in the input package. - success = AttemptSkipLine(); - return RespCommand.INVALID; - } + var optr = ptr; // Read the array length - if (!RespReadUtils.TryReadUnsignedArrayLength(out count, ref ptr, recvBufferPtr + bytesRead)) + if (!RespReadUtils.TryReadUnsignedArrayLength(out count, ref ptr, end)) { - success = false; + commandReceived = false; return RespCommand.INVALID; } // Move readHead to start of command payload - readHead = (int)(ptr - recvBufferPtr); + readHead += (int)(ptr - optr); // Try parsing the most important variable-length commands - cmd = FastParseArrayCommand(ref count, ref specificErrorMessage); + cmd = FastParseArrayCommand(ref count, ptr, (int)(end - ptr), ref specificErrorMessage); if (cmd == RespCommand.NONE) { - cmd = SlowParseCommand(ref count, ref specificErrorMessage, out success); + cmd = SlowParseCommand(ref count, ref specificErrorMessage, out commandReceived, command, subCommand); } // Parsing for command name was successful, but the command is unknown - if (writeErrorOnFailure && success && cmd == RespCommand.INVALID) + if (writeErrorOnFailure && commandReceived && cmd == RespCommand.INVALID) { if (!specificErrorMessage.IsEmpty) { diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index aeb3e966c36..9446276763d 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -88,6 +88,11 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase /// int endReadHead; + /// + /// No redis command (including the terminator) is smaller than this length. + /// + private const int MinimumProcessLength = 4; + internal byte* dcurr, dend; bool toDispose; @@ -430,6 +435,16 @@ internal bool CanRunDebug() networkSender.IsLocalConnection()); } + internal bool CanRunInlineCommands() + { + var enableInlineCommands = storeWrapper.serverOptions.EnableInlineCommands; + + return + (enableInlineCommands == ConnectionProtectionOption.Yes) || + ((enableInlineCommands == ConnectionProtectionOption.Local) && + networkSender.IsLocalConnection()); + } + internal bool CanRunModule() { var enableModuleCommand = storeWrapper.serverOptions.EnableModuleCommand; @@ -568,7 +583,7 @@ private void ProcessMessages() var _origReadHead = readHead; - while (bytesRead - readHead >= 4) + while (bytesRead - readHead >= MinimumProcessLength) { // First, parse the command, making sure we have the entire command available // We use endReadHead to track the end of the current command @@ -1118,75 +1133,6 @@ private bool IsCommandArityValid(string cmdName, int arity, int count) return true; } - ReadOnlySpan GetCommand(out bool success) - { - var ptr = recvBufferPtr + readHead; - var end = recvBufferPtr + bytesRead; - - // Try the command length - if (!RespReadUtils.TryReadUnsignedLengthHeader(out int length, ref ptr, end)) - { - success = false; - return default; - } - - readHead = (int)(ptr - recvBufferPtr); - - // Try to read the command value - ptr += length; - if (ptr + 2 > end) - { - success = false; - return default; - } - - if (*(ushort*)ptr != MemoryMarshal.Read("\r\n"u8)) - { - RespParsingException.ThrowUnexpectedToken(*ptr); - } - - var result = new ReadOnlySpan(recvBufferPtr + readHead, length); - readHead += length + 2; - success = true; - - return result; - } - - ReadOnlySpan GetUpperCaseCommand(out bool success) - { - var ptr = recvBufferPtr + readHead; - var end = recvBufferPtr + bytesRead; - - // Try the command length - if (!RespReadUtils.TryReadUnsignedLengthHeader(out int length, ref ptr, end)) - { - success = false; - return default; - } - - readHead = (int)(ptr - recvBufferPtr); - - // Try to read the command value - ptr += length; - if (ptr + 2 > end) - { - success = false; - return default; - } - - if (*(ushort*)ptr != MemoryMarshal.Read("\r\n"u8)) - { - RespParsingException.ThrowUnexpectedToken(*ptr); - } - - var result = new Span(recvBufferPtr + readHead, length); - readHead += length + 2; - success = true; - - AsciiUtils.ToUpperInPlace(result); - return result; - } - /// /// Attempt to kill this session. /// diff --git a/libs/server/Servers/GarnetServerOptions.cs b/libs/server/Servers/GarnetServerOptions.cs index c935433945c..1902262523a 100644 --- a/libs/server/Servers/GarnetServerOptions.cs +++ b/libs/server/Servers/GarnetServerOptions.cs @@ -442,6 +442,11 @@ public class GarnetServerOptions : ServerOptions /// public ConnectionProtectionOption EnableDebugCommand; + /// + /// Enables parsing inline commands + /// + public ConnectionProtectionOption EnableInlineCommands; + /// /// Enables the MODULE command /// diff --git a/test/Garnet.test/InlineCommandlineTests.cs b/test/Garnet.test/InlineCommandlineTests.cs new file mode 100644 index 00000000000..31615ac9325 --- /dev/null +++ b/test/Garnet.test/InlineCommandlineTests.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace Garnet.test +{ + [NonParallelizable] + [TestFixture] + public class InlineCommandlineTests + { + GarnetServer server; + Socket client; + byte[] buffer; + + [SetUp] + public void Setup() + { + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, disablePubSub: false); + server.Start(); + + client = new Socket(TestUtils.EndPoint.AddressFamily, + SocketType.Stream, + ProtocolType.Tcp); + buffer = new byte[4096]; + } + + [TearDown] + public void TearDown() + { + server.Dispose(); + client.Close(); + client.Dispose(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir); + } + + private async Task Send(string message, string suffix = "\r\n") + { + _ = await client.SendAsync(Encoding.ASCII.GetBytes(message + suffix)); + var received = await client.ReceiveAsync(buffer, SocketFlags.None); + return Encoding.ASCII.GetString(buffer, 0, received); + } + + [Test] + public async Task InlineCommandParseTest() + { + var clientName = "name"; + var key = "key"; + var value = "1"; + + await client.ConnectAsync(TestUtils.EndPoint); + + // Test inline command without arguments + var response = await Send("HELLO"); + ClassicAssert.AreEqual('*', response[0]); + // Test lowercase + response = await Send("hello 3"); + ClassicAssert.AreEqual('%', response[0]); + // Test extranous whitespace + response = await Send("HELLO 2\t "); + ClassicAssert.AreEqual('*', response[0]); + // References accept this too + response = await Send("HElLO 3 SETNAME a SETNAME b"); + ClassicAssert.AreEqual('%', response[0]); + // Test setting client name inline + response = await Send($"HELLO 2 SETNAME {clientName}"); + ClassicAssert.AreEqual('*', response[0]); + + // Should fail due to missing argument. We test such failures to ensure + // readhead is not messed up and commands can be placed afterwards. + response = await Send("CLIENT"); + ClassicAssert.AreEqual("-ERR wrong number of arguments for 'CLIENT' command\r\n", response); + + // Test client name was actually set + response = await Send("CLIENT GETNAME"); + ClassicAssert.AreEqual($"${clientName.Length}\r\n{clientName}\r\n", response); + + // Test inline ping + response = await Send("PING"); + ClassicAssert.AreEqual("+PONG\r\n", response); + + // Test accepting both CRLF and LF as terminating characters. + response = await Send("PING\nPING"); + ClassicAssert.AreEqual("+PONG\r\n+PONG\r\n", response); + // CR is a valid separator + response = await Send("PING\rPING", "\n"); + ClassicAssert.AreEqual("$4\r\nPING\r\n", response); + // As is TAB + response = await Send("PING\tPING"); + ClassicAssert.AreEqual("$4\r\nPING\r\n", response); + + // Test command failure + response = await Send("PIN"); + ClassicAssert.AreEqual("-ERR unknown command\r\n", response); + + // Test ordinary commands + response = await Send($"SET {key} {value}"); + ClassicAssert.AreEqual("+OK\r\n", response); + response = await Send($"GET {key}"); + ClassicAssert.AreEqual($"${value.Length}\r\n{value}\r\n", response); + response = await Send($"EXISTS {key}"); + ClassicAssert.AreEqual(":1\r\n", response); + response = await Send($"DEL {key}"); + ClassicAssert.AreEqual(":1\r\n", response); + + // Test command failure in normal RESP doesn't interfere + response = await Send("*1\r\n$3\r\nPIN"); + ClassicAssert.AreEqual("-ERR unknown command\r\n", response); + + // Test quit + response = await Send("QUIT"); + ClassicAssert.AreEqual("+OK\r\n", response); + } + + [Test] + public async Task InlineCommandEscapeTest() + { + var key = "key"; + + await client.ConnectAsync(TestUtils.EndPoint); + + var response = await Send("PING \\t"); + ClassicAssert.AreEqual("$2\r\n\\t\r\n", response); + // With ' quoting most escapes aren't recognized + response = await Send("PING '\\t'"); + ClassicAssert.AreEqual("$2\r\n\\t\r\n", response); + // Except this one form of escaping + response = await Send("PING '\'\\t\''"); + ClassicAssert.AreEqual("$4\r\n'\\t'\r\n", response); + + // Test escape + response = await Send("PING \"\\t\""); + ClassicAssert.AreEqual("$1\r\n\t\r\n", response); + + // This should lead to quoting failure + response = await Send(@"PING ""\\\"""); + ClassicAssert.AreEqual("-ERR Protocol error: unbalanced quotes in request\r\n", response); + // This should work + response = await Send(@"PING ""\\\\"""); + ClassicAssert.AreEqual("$2\r\n\\\\\r\n", response); + + // Incomplete hex escape 1 + response = await Send("PING \"\\x\""); + ClassicAssert.AreEqual("$1\r\nx\r\n", response); + // Incomplete hex escape 2 + response = await Send("PING \"\\x0\""); + ClassicAssert.AreEqual("$2\r\nx0\r\n", response); + // Invalid hex escape + response = await Send("PING \"\\xGG\""); + ClassicAssert.AreEqual("$3\r\nxGG\r\n", response); + // Complete hex escape + response = await Send("PING \"\\x0A\""); + ClassicAssert.AreEqual("$1\r\n\n\r\n", response); + + // Test escapes in command position + response = await Send(@"""\x50\x49\x4E\x47"""); + ClassicAssert.AreEqual("+PONG\r\n", response); + // Test escapes for lowercase characters + response = await Send(@"""P\i\x6Eg"""); + ClassicAssert.AreEqual("+PONG\r\n", response); + + // Test value being passed + response = await Send($"SET {key} \"a\\x0Ab\""); + ClassicAssert.AreEqual("+OK\r\n", response); + response = await Send($"GET {key}"); + ClassicAssert.AreEqual("$3\r\na\nb\r\n", response); + } + + [Test] + public async Task InlineCommandQuoteTest() + { + await client.ConnectAsync(TestUtils.EndPoint); + + // Test quoted argument + var response = await Send("ping \"hello world\""); + ClassicAssert.AreEqual("$11\r\nhello world\r\n", response); + + // Test quoting failure + // We need to test failures too to be sure readHead is reset right, + // and that there are no leftovers that would interfere with future commands. + response = await Send("PING 'unfinished quote"); + ClassicAssert.AreEqual("-ERR Protocol error: unbalanced quotes in request\r\n", response); + + // Test empty and short strings + response = await Send("ECHO ''"); + ClassicAssert.AreEqual("$0\r\n\r\n", response); + + response = await Send("ECHO 'a'"); + ClassicAssert.AreEqual("$1\r\na\r\n", response); + + // We can even accept commands formed like this + response = await Send("\"PING\" word"); + ClassicAssert.AreEqual("$4\r\nword\r\n", response); + response = await Send("PINg \"hello 'world'!\""); + ClassicAssert.AreEqual("$14\r\nhello 'world'!\r\n", response); + response = await Send("P'ING' ab"); + ClassicAssert.AreEqual("$2\r\nab\r\n", response); + + // Extension + response = await Send("PING '\"'\"''"); + ClassicAssert.AreEqual("$4\r\n\"'\"'\r\n", response); + } + + [Test] + public async Task InlineCommandShortlinesTest() + { + var oldTimeout = client.ReceiveTimeout; + client.ReceiveTimeout = 100; + + await client.ConnectAsync(TestUtils.EndPoint); + + _ = await client.SendAsync(Encoding.ASCII.GetBytes("\r\n")); + try + { + _ = client.Receive(buffer); + Assert.Fail("This should timeout since buffer is not long enough"); + } + catch (SocketException ex) + { + // We expect nothing to be emitted. + ClassicAssert.AreEqual(SocketError.TimedOut, ex.SocketErrorCode); + } + + _ = await client.SendAsync(Encoding.ASCII.GetBytes("PI")); + try + { + _ = client.Receive(buffer); + Assert.Fail("This should timeout since buffer is not long enough"); + } + catch (SocketException ex) + { + // We expect nothing to be emitted. + ClassicAssert.AreEqual(SocketError.TimedOut, ex.SocketErrorCode); + } + + // Because there was no linefeed separator for "PI", we should be able to + // complete the command. + var response = await Send("NG", "\n"); + ClassicAssert.AreEqual("+PONG\r\n", response); + + _ = await client.SendAsync(Encoding.ASCII.GetBytes("A\r\n")); + try + { + _ = client.Receive(buffer); + Assert.Fail("This should timeout since buffer is not long enough"); + } + catch (SocketException ex) + { + // We expect nothing to be emitted due to minimum processing length. + ClassicAssert.AreEqual(SocketError.TimedOut, ex.SocketErrorCode); + } + + response = await Send("PING"); + ClassicAssert.AreEqual("+PONG\r\n", response); + + client.ReceiveTimeout = oldTimeout; + } + } +} \ No newline at end of file diff --git a/test/Garnet.test/RespHashTests.cs b/test/Garnet.test/RespHashTests.cs index 139e84f9b1e..05fcbc8b169 100644 --- a/test/Garnet.test/RespHashTests.cs +++ b/test/Garnet.test/RespHashTests.cs @@ -1401,7 +1401,7 @@ public void CanDoHashExpireWithOptions(string command, string option) } } - #endregion + #endregion #region LightClientTests @@ -1721,7 +1721,7 @@ public void CanDoNOTFOUNDSSKEYLC() [TestCase(3, Description = "RESP3 output")] public async Task HRespOutput(byte respVersion) { - using var c = TestUtils.GetGarnetClientSession(raw: true); + using var c = TestUtils.GetGarnetClientSession(rawResult: true); c.Connect(); var response = await c.ExecuteAsync("HELLO", respVersion.ToString()); diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index 8e614e1f82b..075dc7a5832 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -3220,7 +3220,7 @@ public async Task ZRevRankWithExpiredItems() [TestCase(3, Description = "RESP3 output")] public async Task ZRespOutput(byte respVersion) { - using var c = TestUtils.GetGarnetClientSession(raw: true); + using var c = TestUtils.GetGarnetClientSession(rawResult: true); c.Connect(); var response = await c.ExecuteAsync("HELLO", respVersion.ToString()); @@ -5156,7 +5156,7 @@ public void ZInterResultOrder() lightClientRequest.SendCommand("ZADD zset2 4 e 4 f"); var response = lightClientRequest.SendCommand("ZINTER 2 zset2 zset1 WITHSCORES"); - // ZINTER result should obey sortedset order invariant, + // ZINTER result should obey sortedset order invariant, var expectedResponse = "*4\r\n$1\r\nf\r\n$1\r\n8\r\n$1\r\ne\r\n$1\r\n9"; TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); } diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index 935bd28986a..6c63635ba27 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -289,7 +289,7 @@ public void SingleDump6Bit() var expectedValue = new byte[] { - 0x00, // value type + 0x00, // value type 0x03, // length of payload 0x76, 0x61, 0x6C, // 'v', 'a', 'l' 0x0B, 0x00, // RDB version @@ -2346,7 +2346,7 @@ public void CanDoCommandsInChunks(int bytesSent) response = lightClientRequest.Execute("GET mykey", expectedResponse.Length, bytesSent); ClassicAssert.AreEqual(expectedResponse, response); - // DECR + // DECR expectedResponse = "+OK\r\n"; response = lightClientRequest.Execute("SET mykeydecr 1", expectedResponse.Length, bytesSent); ClassicAssert.AreEqual(expectedResponse, response); @@ -3868,7 +3868,7 @@ public void HelloAuthErrorTest() [TestCase([3, "_\r\n", ",", "%2", '~'], Description = "RESP3 output")] public async Task RespOutputTests(byte respVersion, string expectedResponse, string doublePrefix, string mapPrefix, char setPrefix) { - using var c = TestUtils.GetGarnetClientSession(raw: true); + using var c = TestUtils.GetGarnetClientSession(rawResult: true); c.Connect(); var response = await c.ExecuteAsync("HELLO", respVersion.ToString()); diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index bfff3da0c0f..7af74aa3bc1 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -352,6 +352,7 @@ public static GarnetServer CreateGarnetServer( LoadModuleCS = loadModulePaths, EnableCluster = enableCluster, EnableDebugCommand = enableDebugCommand, + EnableInlineCommands = ConnectionProtectionOption.Local, EnableModuleCommand = enableModuleCommand, EnableReadCache = enableReadCache, EnableObjectStoreReadCache = enableObjectStoreReadCache, @@ -710,6 +711,7 @@ public static GarnetServerOptions GetGarnetServerOptions( DisablePubSub = disablePubSub, DisableObjects = disableObjects, EnableDebugCommand = ConnectionProtectionOption.Yes, + EnableInlineCommands = ConnectionProtectionOption.Local, EnableModuleCommand = ConnectionProtectionOption.Yes, Recover = tryRecover, IndexSize = "1m", @@ -887,7 +889,8 @@ public static GarnetClient GetGarnetClient(EndPoint endpoint = null, bool useTLS return new GarnetClient(endpoint ?? EndPoint, sslOptions, recordLatency: recordLatency); } - public static GarnetClientSession GetGarnetClientSession(bool useTLS = false, bool raw = false, EndPoint endPoint = null) + public static GarnetClientSession GetGarnetClientSession(bool useTLS = false, bool rawResult = false, + EndPoint endPoint = null) { SslClientAuthenticationOptions sslOptions = null; if (useTLS) @@ -900,7 +903,7 @@ public static GarnetClientSession GetGarnetClientSession(bool useTLS = false, bo RemoteCertificateValidationCallback = ValidateServerCertificate, }; } - return new GarnetClientSession(endPoint ?? EndPoint, new(), tlsOptions: sslOptions, rawResult: raw); + return new GarnetClientSession(endPoint ?? EndPoint, new(), tlsOptions: sslOptions, rawResult: rawResult); } public static LightClientRequest CreateRequest(LightClient.OnResponseDelegateUnsafe onReceive = null, bool useTLS = false, CountResponseType countResponseType = CountResponseType.Tokens) diff --git a/website/docs/dev/onboarding.md b/website/docs/dev/onboarding.md index e787356a646..f68f29a8746 100644 --- a/website/docs/dev/onboarding.md +++ b/website/docs/dev/onboarding.md @@ -62,6 +62,18 @@ dotnet run -c Debug -f net8.0 -- --logger-level Trace -m 4g -i 64m 6. A third option is to install Redis-Insight on Windows. Follow the official guide [here](https://redis.com/redis-enterprise/redis-insight/#insight-form). +7. A fourth option is to connect with telnet and just type commands using [inline commands](https://redis.io/docs/latest/develop/reference/protocol-spec/#inline-commands). +However, telnet does not decode the responses, you'll see the raw RESP serialization. +On Windows, telnet defaults to localecho off, you won't see your typing until after first command is sent. +Alternatively, you can type Ctrl-] followed by Enter, and telnet will start echoing. + +Note: By default, Garnet enables inline command parsing only for local connections. +The 'EnableInlineCommands' configuration option can disable it or enable for remote connections too. + +```bash + telnet localhost 6379 +``` + ## Troubleshooting 1. If you need to use TLS in Linux, follow the guide at: