You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kafka.apache.org by rs...@apache.org on 2018/01/30 11:09:10 UTC

[kafka] branch trunk updated: KAFKA-6464: Fix Base64URL encode padding issue under JRE 1.7 (#4455)

This is an automated email from the ASF dual-hosted git repository.

rsivaram pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new b7a0c32  KAFKA-6464: Fix Base64URL encode padding issue under JRE 1.7 (#4455)
b7a0c32 is described below

commit b7a0c327cc24adf38e91c05af6f96a7271f2ab61
Author: Ron Dagostino <rn...@gmail.com>
AuthorDate: Tue Jan 30 06:09:06 2018 -0500

    KAFKA-6464: Fix Base64URL encode padding issue under JRE 1.7 (#4455)
    
    The org.apache.kafka.common.utils.Base64 class defers Base64 encoding/decoding to the java.util.Base64 class beginning with JRE 1.8 but leverages javax.xml.bind.DatatypeConverter under JRE 1.7.  The implementation of the encodeToString(bytes[]) method returned under JRE 1.7 by Base64.urlEncoderNoPadding() blindly removed the last two trailing characters of the Base64 encoding under the assumption that they would always be the string "==" but that is incorrect; padding can be "=", "==" [...]
    
    The commit also adds a Base64.urlDecoder() method that defers to java.util.Base64 under JRE 1.8+ but leverages javax.xml.bind.DatatypeConverter under JRE 1.7.
    
    Finally, there is a unit test to confirm that encode/decode are inverses in both the Base64 and Base64URL cases.
---
 .../java/org/apache/kafka/common/utils/Base64.java | 69 ++++++++++++++++++++--
 .../org/apache/kafka/common/utils/Base64Test.java  | 45 ++++++++++++++
 2 files changed, 109 insertions(+), 5 deletions(-)

diff --git a/clients/src/main/java/org/apache/kafka/common/utils/Base64.java b/clients/src/main/java/org/apache/kafka/common/utils/Base64.java
index e06e1ee..3ab4900 100644
--- a/clients/src/main/java/org/apache/kafka/common/utils/Base64.java
+++ b/clients/src/main/java/org/apache/kafka/common/utils/Base64.java
@@ -50,6 +50,10 @@ public final class Base64 {
         return FACTORY.decoder();
     }
 
+    public static Decoder urlDecoder() {
+        return FACTORY.urlDecoder();
+    }
+
     /* Contains a subset of methods from java.util.Base64.Encoder (introduced in Java 8) */
     public interface Encoder {
         String encodeToString(byte[] bytes);
@@ -63,6 +67,7 @@ public final class Base64 {
     private interface Factory {
         Encoder urlEncoderNoPadding();
         Encoder encoder();
+        Decoder urlDecoder();
         Decoder decoder();
     }
 
@@ -71,10 +76,12 @@ public final class Base64 {
         // Static final MethodHandles are optimised better by HotSpot
         private static final MethodHandle URL_ENCODE_NO_PADDING;
         private static final MethodHandle ENCODE;
+        private static final MethodHandle URL_DECODE;
         private static final MethodHandle DECODE;
 
         private static final Encoder URL_ENCODER_NO_PADDING;
         private static final Encoder ENCODER;
+        private static final Decoder URL_DECODER;
         private static final Decoder DECODER;
 
         static {
@@ -123,6 +130,17 @@ public final class Base64 {
                     throw (RuntimeException) throwable;
                 }
 
+                MethodHandle getUrlDecoder = lookup.findStatic(base64Class, "getUrlDecoder",
+                        MethodType.methodType(juDecoderClass));
+                MethodHandle urlDecode = lookup.findVirtual(juDecoderClass, "decode",
+                        MethodType.methodType(byte[].class, String.class));
+                try {
+                    URL_DECODE = urlDecode.bindTo(getUrlDecoder.invoke());
+                } catch (Throwable throwable) {
+                    // Invoked method doesn't throw checked exceptions, so safe to cast
+                    throw (RuntimeException) throwable;
+                }
+                
                 URL_ENCODER_NO_PADDING = new Encoder() {
                     @Override
                     public String encodeToString(byte[] bytes) {
@@ -147,6 +165,18 @@ public final class Base64 {
                     }
                 };
 
+                URL_DECODER = new Decoder() {
+                    @Override
+                    public byte[] decode(String string) {
+                        try {
+                            return (byte[]) URL_DECODE.invokeExact(string);
+                        } catch (Throwable throwable) {
+                            // Invoked method doesn't throw checked exceptions, so safe to cast
+                            throw (RuntimeException) throwable;
+                        }
+                    }
+                };
+
                 DECODER = new Decoder() {
                     @Override
                     public byte[] decode(String string) {
@@ -179,6 +209,11 @@ public final class Base64 {
         public Decoder decoder() {
             return DECODER;
         }
+
+        @Override
+        public Decoder urlDecoder() {
+            return URL_DECODER;
+        }
     }
 
     private static class Java7Factory implements Factory {
@@ -205,12 +240,15 @@ public final class Base64 {
 
             @Override
             public String encodeToString(byte[] bytes) {
+                if (bytes.length == 0)
+                    return "";
                 String base64EncodedUUID = Java7Factory.encodeToString(bytes);
-                //Convert to URL safe variant by replacing + and / with - and _ respectively.
-                String urlSafeBase64EncodedUUID = base64EncodedUUID.replace("+", "-")
-                        .replace("/", "_");
-                // Remove the "==" padding at the end.
-                return urlSafeBase64EncodedUUID.substring(0, urlSafeBase64EncodedUUID.length() - 2);
+                // Convert to URL safe variant by replacing + and / with - and _ respectively.
+                String urlSafeBase64EncodedUUID = base64EncodedUUID.replace("+", "-").replace("/", "_");
+                // Remove any "=" or "==" padding at the end.
+                // Note that length will be at least 4 here.
+                int index = urlSafeBase64EncodedUUID.indexOf('=', urlSafeBase64EncodedUUID.length() - 2);
+                return index > 0 ? urlSafeBase64EncodedUUID.substring(0, index) : urlSafeBase64EncodedUUID;
             }
 
         };
@@ -234,6 +272,22 @@ public final class Base64 {
             }
         };
 
+        public static final Decoder URL_DECODER = new Decoder() {
+            @Override
+            public byte[] decode(String string) {
+                try {
+                    // Convert from URL safe variant by replacing - and _ with + and / respectively,
+                    // and append "=" or "==" padding; then decode.
+                    String unpadded = string.replace("-", "+").replace("_", "/");
+                    int padLength = 4 - (unpadded.length() & 3);
+                    return (byte[]) PARSE.invokeExact(padLength > 2 ? unpadded : unpadded + "==".substring(0, padLength));
+                } catch (Throwable throwable) {
+                    // Invoked method doesn't throw checked exceptions, so safe to cast
+                    throw (RuntimeException) throwable;
+                }
+            }
+        };
+
         private static String encodeToString(byte[] bytes) {
             try {
                 return (String) PRINT.invokeExact(bytes);
@@ -254,6 +308,11 @@ public final class Base64 {
         }
 
         @Override
+        public Decoder urlDecoder() {
+            return URL_DECODER;
+        }
+
+        @Override
         public Decoder decoder() {
             return DECODER;
         }
diff --git a/clients/src/test/java/org/apache/kafka/common/utils/Base64Test.java b/clients/src/test/java/org/apache/kafka/common/utils/Base64Test.java
new file mode 100644
index 0000000..9dcd15a
--- /dev/null
+++ b/clients/src/test/java/org/apache/kafka/common/utils/Base64Test.java
@@ -0,0 +1,45 @@
+/*
+ * 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.kafka.common.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.kafka.common.utils.Base64.Decoder;
+import org.apache.kafka.common.utils.Base64.Encoder;
+import org.junit.Test;
+
+public class Base64Test {
+
+    @Test
+    public void testBase64UrlEncodeDecode() {
+        confirmInversesForAllThreePaddingCases(Base64.urlEncoderNoPadding(), Base64.urlDecoder());
+    }
+
+    @Test
+    public void testBase64EncodeDecode() {
+        confirmInversesForAllThreePaddingCases(Base64.encoder(), Base64.decoder());
+    }
+
+    private static void confirmInversesForAllThreePaddingCases(Encoder encoder, Decoder decoder) {
+        for (String text : new String[] {"", "a", "ab", "abc"}) {
+            assertEquals(text, new String(decoder.decode(encoder.encodeToString(text.getBytes(StandardCharsets.UTF_8))),
+                    StandardCharsets.UTF_8));
+        }
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
rsivaram@apache.org.