Geode-8677: Confirm binary data storage (#5696)

Co-authored-by: Ray Ingles <ringles@vmware.com>
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/key/AbstractKeysIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/key/AbstractKeysIntegrationTest.java
index cd8d32a..a726ad8 100644
--- a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/key/AbstractKeysIntegrationTest.java
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/key/AbstractKeysIntegrationTest.java
@@ -18,6 +18,9 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -91,11 +94,21 @@
   }
 
   @Test
-  public void givenBinaryValue_withExactMatch_preservesBinaryData() {
+  public void givenBinaryValue_withExactMatch_preservesBinaryData()
+      throws UnsupportedEncodingException {
+    String chinese_utf16 = "子";
+    byte[] utf16encodedBytes = chinese_utf16.getBytes("UTF-16");
     byte[] stringKey =
         new byte[] {(byte) 0xac, (byte) 0xed, 0, 4, 0, 5, 's', 't', 'r', 'i', 'n', 'g', '1'};
-    jedis.set(stringKey, stringKey);
-    assertThat(jedis.keys(stringKey)).containsExactlyInAnyOrder(stringKey);
+    byte[] allByteArray = new byte[utf16encodedBytes.length + stringKey.length];
+
+    ByteBuffer buff = ByteBuffer.wrap(allByteArray);
+    buff.put(utf16encodedBytes);
+    buff.put(stringKey);
+    byte[] combined = buff.array();
+
+    jedis.set(combined, combined);
+    assertThat(jedis.keys("*".getBytes())).containsExactlyInAnyOrder(combined);
   }
 
   @Test
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractAppendIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractAppendIntegrationTest.java
index 90203b2..4b012d7 100755
--- a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractAppendIntegrationTest.java
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractAppendIntegrationTest.java
@@ -16,6 +16,9 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -101,6 +104,22 @@
     assertThat(result).isEqualTo(doubleBlob);
   }
 
+  @Test
+  public void testAppend_withUTF16KeyAndValue() throws IOException {
+    String test_utf16_string = "最𐐷𤭢";
+    byte[] testBytes = test_utf16_string.getBytes(StandardCharsets.UTF_16);
+
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    output.write(testBytes);
+    output.write(testBytes);
+    byte[] appendedBytes = output.toByteArray();
+
+    jedis.set(testBytes, testBytes);
+    jedis.append(testBytes, testBytes);
+    byte[] result = jedis.get(testBytes);
+    assertThat(result).isEqualTo(appendedBytes);
+  }
+
   private String randString() {
     return Long.toHexString(Double.doubleToLongBits(Math.random()));
   }
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractGetRangeIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractGetRangeIntegrationTest.java
index 2a02f67..5d762f0 100755
--- a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractGetRangeIntegrationTest.java
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractGetRangeIntegrationTest.java
@@ -18,6 +18,8 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.nio.charset.StandardCharsets;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -146,6 +148,75 @@
   }
 
   @Test
+  public void testGetRange_whenValidSubrangeSpecified_binaryData_returnsAppropriateSubstring() {
+    byte[] keyWith13Chars =
+        new byte[] {(byte) 0xac, (byte) 0xed, 0, 4, 0, 5, 's', 't', 'r', 'i', 'n', 'g', '1'};
+
+    jedis.set(keyWith13Chars, keyWith13Chars);
+
+    byte[] fromStartToBeforeEnd = jedis.getrange(keyWith13Chars, 0, 10);
+    assertThat(fromStartToBeforeEnd)
+        .isEqualTo(new byte[] {(byte) 0xac, (byte) 0xed, 0, 4, 0, 5, 's', 't', 'r', 'i', 'n'});
+
+    byte[] fromStartByNegativeOffsetToBeforeEnd = jedis.getrange(keyWith13Chars, -19, 10);
+    assertThat(fromStartByNegativeOffsetToBeforeEnd)
+        .isEqualTo(new byte[] {(byte) 0xac, (byte) 0xed, 0, 4, 0, 5, 's', 't', 'r', 'i', 'n'});
+
+    byte[] fromStartToBeforeEndByNegativeOffset = jedis.getrange(keyWith13Chars, 0, -3);
+    assertThat(fromStartToBeforeEndByNegativeOffset)
+        .isEqualTo(new byte[] {(byte) 0xac, (byte) 0xed, 0, 4, 0, 5, 's', 't', 'r', 'i', 'n'});
+
+    byte[] fromAfterStartToBeforeEnd = jedis.getrange(keyWith13Chars, 2, 10);
+    assertThat(fromAfterStartToBeforeEnd)
+        .isEqualTo(new byte[] {0, 4, 0, 5, 's', 't', 'r', 'i', 'n'});
+
+    byte[] fromAfterStartByNegativeOffsetToBeforeEndByNegativeOffset =
+        jedis.getrange(keyWith13Chars, -10, -2);
+    assertThat(fromAfterStartByNegativeOffsetToBeforeEndByNegativeOffset)
+        .isEqualTo(new byte[] {4, 0, 5, 's', 't', 'r', 'i', 'n', 'g'});
+
+    byte[] fromAfterStartToEnd = jedis.getrange(keyWith13Chars, 2, 13);
+    assertThat(fromAfterStartToEnd)
+        .isEqualTo(new byte[] {0, 4, 0, 5, 's', 't', 'r', 'i', 'n', 'g', '1'});
+
+    byte[] fromAfterStartToEndByNegativeOffset = jedis.getrange(keyWith13Chars, 2, -1);
+    assertThat(fromAfterStartToEndByNegativeOffset)
+        .isEqualTo(new byte[] {0, 4, 0, 5, 's', 't', 'r', 'i', 'n', 'g', '1'});
+  }
+
+  @Test
+  public void testGetRange_whenValidSubrangeSpecified_utf16Data_returnsAppropriateSubstring() {
+    String utf16string = "最𐐷𤭢";
+    byte[] key = utf16string.getBytes(StandardCharsets.UTF_16);
+
+    jedis.set(key, key);
+
+    byte[] fromStartToBeforeEnd = jedis.getrange(key, 0, 4);
+    assertThat(fromStartToBeforeEnd).isEqualTo(new byte[] {-2, -1, 103, 0, -40});
+
+    byte[] fromStartByNegativeOffsetToBeforeEnd = jedis.getrange(key, -19, 4);
+    assertThat(fromStartByNegativeOffsetToBeforeEnd).isEqualTo(new byte[] {-2, -1, 103, 0, -40});
+
+    byte[] fromStartToBeforeEndByNegativeOffset = jedis.getrange(key, 0, -2);
+    assertThat(fromStartToBeforeEndByNegativeOffset)
+        .isEqualTo(new byte[] {-2, -1, 103, 0, -40, 1, -36, 55, -40, 82, -33});
+
+    byte[] fromAfterStartToBeforeEnd = jedis.getrange(key, 2, 4);
+    assertThat(fromAfterStartToBeforeEnd).isEqualTo(new byte[] {103, 0, -40});
+
+    byte[] fromAfterStartByNegativeOffsetToBeforeEndByNegativeOffset = jedis.getrange(key, -10, -2);
+    assertThat(fromAfterStartByNegativeOffsetToBeforeEndByNegativeOffset)
+        .isEqualTo(new byte[] {103, 0, -40, 1, -36, 55, -40, 82, -33});
+
+    byte[] fromAfterStartToEnd = jedis.getrange(key, 2, 10);
+    assertThat(fromAfterStartToEnd).isEqualTo(new byte[] {103, 0, -40, 1, -36, 55, -40, 82, -33});
+
+    byte[] fromAfterStartToEndByNegativeOffset = jedis.getrange(key, 2, -1);
+    assertThat(fromAfterStartToEndByNegativeOffset)
+        .isEqualTo(new byte[] {103, 0, -40, 1, -36, 55, -40, 82, -33, 98});
+  }
+
+  @Test
   public void testGetRange_rangeIsInvalid_returnsEmptyString() {
     String key = "key";
     String valueWith19Characters = "abc123babyyouknowme";
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractLettuceAppendIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractLettuceAppendIntegrationTest.java
new file mode 100644
index 0000000..b3cc3f1
--- /dev/null
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractLettuceAppendIntegrationTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.geode.redis.internal.executor.string;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.api.sync.RedisCommands;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import org.apache.geode.test.dunit.rules.RedisPortSupplier;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public abstract class AbstractLettuceAppendIntegrationTest implements RedisPortSupplier {
+
+  protected RedisClient client;
+
+  @ClassRule
+  public static ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  @Before
+  public void before() {
+    client = RedisClient.create("redis://localhost:" + getPort());
+  }
+
+  @After
+  public void after() {
+    client.shutdown();
+  }
+
+  @Test
+  public void testAppend_withUTF16KeyAndValue() {
+    String test_utf16_string = "最𐐷𤭢";
+    String double_utf16_string = test_utf16_string + test_utf16_string;
+
+    StatefulRedisConnection<String, String> redisConnection = client.connect();
+    RedisCommands<String, String> syncCommands = redisConnection.sync();
+
+    syncCommands.set(test_utf16_string, test_utf16_string);
+    syncCommands.append(test_utf16_string, test_utf16_string);
+    String result = syncCommands.get(test_utf16_string);
+    assertThat(result).isEqualTo(double_utf16_string);
+  }
+}
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractStrLenIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractStrLenIntegrationTest.java
index 600430a..f9b6b75 100755
--- a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractStrLenIntegrationTest.java
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/AbstractStrLenIntegrationTest.java
@@ -17,6 +17,8 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.nio.charset.StandardCharsets;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -100,6 +102,15 @@
   }
 
   @Test
+  public void testStrlen_withUTF16BinaryData() {
+    String test_utf16_string = "最𐐷𤭢";
+    byte[] testBytes = test_utf16_string.getBytes(StandardCharsets.UTF_16);
+    jedis.set(testBytes, testBytes);
+
+    assertThat(jedis.strlen(testBytes)).isEqualTo(12);
+  }
+
+  @Test
   public void testStrlen_withIntData() {
     byte[] key = new byte[] {0};
     byte[] value = new byte[] {1, 0, 0};
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/LettuceAppendIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/LettuceAppendIntegrationTest.java
new file mode 100755
index 0000000..47fb283
--- /dev/null
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/string/LettuceAppendIntegrationTest.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.executor.string;
+
+import org.junit.ClassRule;
+
+import org.apache.geode.redis.GeodeRedisServerRule;
+
+public class LettuceAppendIntegrationTest extends AbstractLettuceAppendIntegrationTest {
+
+  @ClassRule
+  public static GeodeRedisServerRule server = new GeodeRedisServerRule();
+
+  @Override
+  public int getPort() {
+    return server.getPort();
+  }
+
+}
diff --git a/geode-redis/src/main/java/org/apache/geode/redis/internal/netty/Coder.java b/geode-redis/src/main/java/org/apache/geode/redis/internal/netty/Coder.java
index fdcc460..79a609f 100644
--- a/geode-redis/src/main/java/org/apache/geode/redis/internal/netty/Coder.java
+++ b/geode-redis/src/main/java/org/apache/geode/redis/internal/netty/Coder.java
@@ -15,7 +15,6 @@
  */
 package org.apache.geode.redis.internal.netty;
 
-import java.io.UnsupportedEncodingException;
 import java.math.BigInteger;
 import java.text.DecimalFormat;
 import java.util.Collection;
@@ -329,11 +328,7 @@
     if (bytes == null) {
       return null;
     }
-    try {
-      return new String(bytes, CHARSET);
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException(e);
-    }
+    return new String(bytes);
   }
 
   public static String doubleToString(double d) {
@@ -355,11 +350,7 @@
     if (string == null) {
       return null;
     }
-    try {
-      return string.getBytes(CHARSET);
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException(e);
-    }
+    return string.getBytes();
   }
 
   /*