Repository: redis/lettuce
Pull request #3110 (2024-12-30 15:07:19)

Handle UTF-8 characters in command arguments #3071
<!--
Thank you for proposing a pull request. This template will guide you through the essential steps necessary for a pull request.
-->
Make sure that:

- [x] You have read the [contribution guidelines](https://github.com/lettuce-io/lettuce-core/blob/main/.github/CONTRIBUTING.md).
- [x] You have created a feature request first to discuss your contribution intent. Please reference the feature request ticket number in the pull request.
- [x] You applied code formatting rules using the `mvn formatter:format` target. Don’t submit any formatting related changes.
- [x] You submit test cases (unit or integration tests) that back your changes.

 <!--
Great! Live long and prosper.
-->



Patch:
diff --git a/src/main/java/io/lettuce/core/protocol/CommandArgs.java b/src/main/java/io/lettuce/core/protocol/CommandArgs.java
index da5f89785..addc78a8d 100644
--- a/src/main/java/io/lettuce/core/protocol/CommandArgs.java
+++ b/src/main/java/io/lettuce/core/protocol/CommandArgs.java
@@ -20,6 +20,7 @@
 package io.lettuce.core.protocol;
 
 import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Base64;
@@ -53,7 +54,7 @@ import io.netty.buffer.UnpooledByteBufAllocator;
  */
 public class CommandArgs<K, V> {
 
-    static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.US_ASCII);
+    static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
 
     protected final RedisCodec<K, V> codec;
 
@@ -609,16 +610,9 @@ public class CommandArgs<K, V> {
         }
 
         static void writeString(ByteBuf target, String value) {
+            byte[] output = value.getBytes(StandardCharsets.UTF_8);
 
-            target.writeByte('$');
-
-            IntegerArgument.writeInteger(target, value.length());
-            target.writeBytes(CRLF);
-
-            for (int i = 0; i < value.length(); i++) {
-                target.writeByte((byte) value.charAt(i));
-            }
-            target.writeBytes(CRLF);
+            BytesArgument.writeBytes(target, output);
         }
 
         @Override
@@ -646,16 +640,11 @@ public class CommandArgs<K, V> {
         }
 
         static void writeString(ByteBuf target, char[] value) {
+            final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(value));
+            final byte[] output = new byte[byteBuffer.remaining()];
+            byteBuffer.get(output);
 
-            target.writeByte('$');
-
-            IntegerArgument.writeInteger(target, value.length);
-            target.writeBytes(CRLF);
-
-            for (char c : value) {
-                target.writeByte((byte) c);
-            }
-            target.writeBytes(CRLF);
+            BytesArgument.writeBytes(target, output);
         }
 
         @Override


Test patch:
diff --git a/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java b/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java
index 864a2103b..77d3f8edd 100644
--- a/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java
@@ -37,6 +37,9 @@ class AuthenticationIntegrationTests extends TestSupport {
 
         connection.sync().dispatch(CommandType.ACL, new StatusOutput<>(StringCodec.UTF8),
                 new CommandArgs<>(StringCodec.UTF8).add("SETUSER").add("john").add("on").add(">foobared").add("-@all"));
+
+        connection.sync().dispatch(CommandType.ACL, new StatusOutput<>(StringCodec.UTF8),
+                new CommandArgs<>(StringCodec.UTF8).add("SETUSER").add("日本語").add("on").add(">日本語").add("+@all"));
     }
 
     @Test
@@ -71,4 +74,16 @@ class AuthenticationIntegrationTests extends TestSupport {
         });
     }
 
+    @Test
+    @Inject
+    void authAsJapanese(RedisClient client) {
+
+        RedisURI uri = RedisURI.builder().withHost(TestSettings.host()).withPort(TestSettings.port())
+                .withAuthentication("日本語", "日本語".toCharArray()).build();
+
+        StatefulRedisConnection<String, String> connection = client.connect(uri);
+        assertThat(connection.sync().ping()).isEqualTo("PONG");
+        connection.close();
+    }
+
 }
diff --git a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
index d8e71cffb..2d4f1cf40 100644
--- a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
+++ b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
@@ -32,6 +32,17 @@ class RedisCommandBuilderUnitTests {
 
     RedisCommandBuilder<String, String> sut = new RedisCommandBuilder<>(StringCodec.UTF8);
 
+    @Test()
+    void shouldCorrectlyConstructHello() {
+
+        Command<String, String, ?> command = sut.hello(3, "日本語", "日本語".toCharArray(), null);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*5\r\n" + "$5\r\n" + "HELLO\r\n" + "$1\r\n" + "3\r\n"
+                + "$4\r\n" + "AUTH\r\n" + "$9\r\n" + "日本語\r\n" + "$9\r\n" + "日本語\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructXreadgroup() {
 
diff --git a/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java
index 7fbeb7cf9..4d154dc0f 100644
--- a/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java
@@ -131,7 +131,7 @@ public class AclCommandIntegrationTests extends TestSupport {
 
     @Test
     void aclList() {
-        assertThat(redis.aclList()).hasSize(1).first().asString().contains("user default");
+        assertThat(redis.aclList()).hasSize(2).first().asString().contains("user default");
     }
 
     @Test
@@ -161,7 +161,7 @@ public class AclCommandIntegrationTests extends TestSupport {
 
     @Test
     void aclUsers() {
-        assertThat(redis.aclUsers()).hasSize(1).first().isEqualTo("default");
+        assertThat(redis.aclUsers()).hasSize(2).first().isEqualTo("default");
     }
 
     @Test