diff --git a/classlib/src/main/java/org/teavm/classlib/impl/Base64Impl.java b/classlib/src/main/java/org/teavm/classlib/impl/Base64Impl.java index c4d480b52..4c1705f53 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/Base64Impl.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/Base64Impl.java @@ -18,33 +18,47 @@ package org.teavm.classlib.impl; import java.util.Arrays; public final class Base64Impl { - private static byte[] alphabet = new byte[64]; - private static int[] reverse = new int[256]; + public static byte[] alphabet = new byte[64]; + public static byte[] urlAlphabet = new byte[64]; + public static int[] reverse = new int[256]; + public static int[] urlReverse = new int[256]; static { int i = 0; for (char c = 'A'; c <= 'Z'; ++c) { - alphabet[i++] = (byte) c; + alphabet[i] = (byte) c; + urlAlphabet[i] = (byte) c; + i++; } for (char c = 'a'; c <= 'z'; ++c) { - alphabet[i++] = (byte) c; + alphabet[i] = (byte) c; + urlAlphabet[i] = (byte) c; + i++; } for (char c = '0'; c <= '9'; ++c) { - alphabet[i++] = (byte) c; + alphabet[i] = (byte) c; + urlAlphabet[i] = (byte) c; + i++; } - alphabet[i++] = '+'; - alphabet[i++] = '/'; + alphabet[i] = '+'; + urlAlphabet[i] = '-'; + i++; + alphabet[i] = '/'; + urlAlphabet[i] = '_'; + i++; Arrays.fill(reverse, -1); + Arrays.fill(urlReverse, -1); for (i = 0; i < alphabet.length; ++i) { reverse[alphabet[i]] = i; + urlReverse[urlAlphabet[i]] = i; } } private Base64Impl() { } - public static byte[] decode(byte[] text) { + public static byte[] decode(byte[] text, int[] mapping) { int outputSize = (text.length / 4) * 3; int rem = text.length % 4; if (rem == 2 || rem == 3) { @@ -55,11 +69,15 @@ public final class Base64Impl { --outputSize; } byte[] output = new byte[outputSize]; - decode(text, output); + decode(text, output, mapping); return output; } - public static void decode(byte[] text, byte[] output) { + public static byte[] decode(byte[] text) { + return decode(text, reverse); + } + + public static void decode(byte[] text, byte[] output, int[] mapping) { int inputSize = text.length; int i; for (i = text.length - 1; i >= 0 && text[i] == '='; --i) { @@ -70,10 +88,10 @@ public final class Base64Impl { i = 0; int j = 0; while (i < triples) { - int a = decode(text[i++]); - int b = decode(text[i++]); - int c = decode(text[i++]); - int d = decode(text[i++]); + int a = decode(mapping, text[i++]); + int b = decode(mapping, text[i++]); + int c = decode(mapping, text[i++]); + int d = decode(mapping, text[i++]); int out = (a << 18) | (b << 12) | (c << 6) | d; output[j++] = (byte) (out >>> 16); output[j++] = (byte) (out >>> 8); @@ -82,23 +100,23 @@ public final class Base64Impl { int rem = inputSize - i; if (rem == 2) { - int a = decode(text[i]); - int b = decode(text[i + 1]); + int a = decode(mapping, text[i]); + int b = decode(mapping, text[i + 1]); output[j] = (byte) ((a << 2) | (b >>> 4)); } else if (rem == 3) { - int a = decode(text[i]); - int b = decode(text[i + 1]); - int c = decode(text[i + 2]); + int a = decode(mapping, text[i]); + int b = decode(mapping, text[i + 1]); + int c = decode(mapping, text[i + 2]); output[j] = (byte) ((a << 2) | (b >>> 4)); output[j + 1] = (byte) ((b << 4) | (c >>> 2)); } } - private static int decode(byte c) { - return reverse[c]; + private static int decode(int[] mapping, byte c) { + return mapping[c]; } - public static byte[] encode(byte[] data, boolean pad) { + public static byte[] encode(byte[] data, byte[] mapping, boolean pad) { int outputSize = ((data.length + 2) / 3) * 4; if (!pad) { int rem = data.length % 3; @@ -107,38 +125,42 @@ public final class Base64Impl { } } byte[] output = new byte[outputSize]; - encode(data, output, pad); + encode(data, output, mapping, pad); return output; } - public static int encode(byte[] data, byte[] output, boolean pad) { + public static byte[] encode(byte[] data, boolean pad) { + return encode(data, alphabet, pad); + } + + public static int encode(byte[] data, byte[] output, byte[] mapping, boolean pad) { int triples = (data.length / 3) * 3; int i = 0; int j; for (j = 0; j < triples;) { - output[i++] = encode((byte) (data[j] >>> 2)); - output[i++] = encode((byte) ((data[j] << 4) | ((data[j + 1] & 0xFF) >>> 4))); + output[i++] = encode(mapping, (byte) (data[j] >>> 2)); + output[i++] = encode(mapping, (byte) ((data[j] << 4) | ((data[j + 1] & 0xFF) >>> 4))); ++j; - output[i++] = encode((byte) ((data[j] << 2) | ((data[j + 1] & 0xFF) >>> 6))); + output[i++] = encode(mapping, (byte) ((data[j] << 2) | ((data[j + 1] & 0xFF) >>> 6))); ++j; - output[i++] = encode(data[j]); + output[i++] = encode(mapping, data[j]); ++j; } int rem = data.length - j; if (rem == 1) { - output[i++] = encode((byte) (data[j] >>> 2)); - output[i++] = encode((byte) (data[j] << 4)); + output[i++] = encode(mapping, (byte) (data[j] >>> 2)); + output[i++] = encode(mapping, (byte) (data[j] << 4)); if (pad) { output[i++] = '='; output[i++] = '='; } } else if (rem == 2) { - output[i++] = encode((byte) (data[j] >>> 2)); - output[i++] = encode((byte) ((data[j] << 4) | (data[j + 1] >>> 4))); + output[i++] = encode(mapping, (byte) (data[j] >>> 2)); + output[i++] = encode(mapping, (byte) ((data[j] << 4) | ((data[j + 1] & 0xFF) >>> 4))); ++j; - output[i++] = encode((byte) (data[j] << 2)); + output[i++] = encode(mapping, (byte) (data[j] << 2)); if (pad) { output[i++] = '='; } @@ -147,7 +169,7 @@ public final class Base64Impl { return i; } - private static byte encode(byte b) { - return alphabet[b & 63]; + private static byte encode(byte[] mapping, byte b) { + return mapping[b & 63]; } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TBase64.java b/classlib/src/main/java/org/teavm/classlib/java/util/TBase64.java new file mode 100644 index 000000000..aa8b6b376 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TBase64.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 ihromant. + * + * Licensed 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.teavm.classlib.java.util; + +import org.teavm.classlib.impl.Base64Impl; + +public class TBase64 { + public static Encoder getEncoder() { + return new Encoder(Base64Impl.alphabet, true); + } + + public static Encoder getUrlEncoder() { + return new Encoder(Base64Impl.urlAlphabet, false); + } + + public static class Encoder { + private final byte[] mapping; + private final boolean padding; + + private Encoder(byte[] mapping, boolean padding) { + this.mapping = mapping; + this.padding = padding; + } + + public byte[] encode(byte[] src) { + return Base64Impl.encode(src, mapping, padding); + } + } + + public Decoder getDecoder() { + return new Decoder(Base64Impl.reverse); + } + + public Decoder getUrlDecoder() { + return new Decoder(Base64Impl.urlReverse); + } + + public static class Decoder { + private final int[] mapping; + + private Decoder(int[] mapping) { + this.mapping = mapping; + } + + public byte[] decode(byte[] src) { + return Base64Impl.decode(src, mapping); + } + } +} diff --git a/classlib/src/test/java/org/teavm/classlib/impl/Base64Test.java b/classlib/src/test/java/org/teavm/classlib/impl/Base64Test.java index 8eab16440..20f05fc82 100644 --- a/classlib/src/test/java/org/teavm/classlib/impl/Base64Test.java +++ b/classlib/src/test/java/org/teavm/classlib/impl/Base64Test.java @@ -33,6 +33,7 @@ public class Base64Test { assertEquals("qwerty", decode("cXdlcnR5")); assertEquals("qwertyu", decode("cXdlcnR5dQ==")); assertEquals("qwertyu", decode("cXdlcnR5dQ")); + assertEquals("юзернейм:пароль", decode("0Y7Qt9C10YDQvdC10LnQvDrQv9Cw0YDQvtC70Yw=")); } @Test @@ -44,6 +45,7 @@ public class Base64Test { assertEquals("cXdlcnQ=", encode("qwert")); assertEquals("cXdlcnR5", encode("qwerty")); assertEquals("cXdlcnR5dQ==", encode("qwertyu")); + assertEquals("0Y7Qt9C10YDQvdC10LnQvDrQv9Cw0YDQvtC70Yw=", encode("юзернейм:пароль")); } @Test diff --git a/tests/src/test/java/org/teavm/classlib/java/util/Base64Test.java b/tests/src/test/java/org/teavm/classlib/java/util/Base64Test.java new file mode 100644 index 000000000..c584aaad7 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/util/Base64Test.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023 ihromant. + * + * Licensed 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.teavm.classlib.impl; + +import static org.junit.Assert.*; +import java.io.UnsupportedEncodingException; +import org.junit.Test; + +@RunWith(TeaVMTestRunner.class) +@WholeClassCompilation +public class Base64Test { + @Test + public void decoderWorks() { + Decoder decoder = Base64.getDecoder(); + assertEquals("q", decoder.decode("cQ==")); + assertEquals("qw", decoder.decode("cXc=")); + assertEquals("qwe", decoder.decode("cXdl")); + assertEquals("qwer", decoder.decode("cXdlcg==")); + assertEquals("qwert", decoder.decode("cXdlcnQ=")); + assertEquals("qwerty", decoder.decode("cXdlcnR5")); + assertEquals("qwertyu", decoder.decode("cXdlcnR5dQ==")); + assertEquals("юзернейм:пароль", decoder.decode("0Y7Qt9C10YDQvdC10LnQvDrQv9Cw0YDQvtC70Yw=")); + } + + @Test + public void encoderWorks() { + Encoder encoder = Base64.getEncoder(); + assertEquals("cQ==", encoder.encode("q")); + assertEquals("cXc=", encoder.encode("qw")); + assertEquals("cXdl", encoder.encode("qwe")); + assertEquals("cXdlcg==", encoder.encode("qwer")); + assertEquals("cXdlcnQ=", encoder.encode("qwert")); + assertEquals("cXdlcnR5", encoder.encode("qwerty")); + assertEquals("cXdlcnR5dQ==", encoder.encode("qwertyu")); + assertEquals("0Y7Qt9C10YDQvdC10LnQvDrQv9Cw0YDQvtC70Yw=", encoder.encode("юзернейм:пароль")); + } + + @Test + public void urlDecoderWorks() { + Decoder decoder = Base64.getUrlDecoder(); + assertEquals("q", decoder.decode("cQ")); + assertEquals("qw", decoder.decode("cXc")); + assertEquals("qwe", decoder.decode("cXdl")); + assertEquals("qwer", decoder.decode("cXdlcg")); + assertEquals("qwerty", decoder.decode("cXdlcnR5")); + assertEquals("qwertyu", decoder.decode("cXdlcnR5dQ")); + assertEquals("юзернейм:пароль", decoder.decode("0Y7Qt9C10YDQvdC10LnQvDrQv9Cw0YDQvtC70Yw")); + } + + @Test + public void urlEncoderWorks() { + Encoder encoder = Base64.getUrlEncoder(); + assertEquals("cQ", encoder.encodeNoPad("q")); + assertEquals("cXc", encoder.encodeNoPad("qw")); + assertEquals("cXdl", encoder.encodeNoPad("qwe")); + assertEquals("cXdlcg", encoder.encodeNoPad("qwer")); + assertEquals("cXdlcnQ", encoder.encodeNoPad("qwert")); + assertEquals("cXdlcnR5", encoder.encodeNoPad("qwerty")); + assertEquals("cXdlcnR5dQ", encoder.encodeNoPad("qwertyu")); + assertEquals("0Y7Qt9C10YDQvdC10LnQvDrQv9Cw0YDQvtC70Yw", encoder.encode("юзернейм:пароль")); + } + + private String decode(String text) { + try { + return new String(Base64Impl.decode(text.getBytes("UTF-8")), "UTF-8"); + } catch (UnsupportedEncodingException e) { + return ""; + } + } + + private String encode(String text) { + try { + return new String(Base64Impl.encode(text.getBytes("UTF-8"), true), "UTF-8"); + } catch (UnsupportedEncodingException e) { + return ""; + } + } + + private String encodeNoPad(String text) { + try { + return new String(Base64Impl.encode(text.getBytes("UTF-8"), false), "UTF-8"); + } catch (UnsupportedEncodingException e) { + return ""; + } + } +}