Last active
October 29, 2024 14:53
-
-
Save MarkusKramer/4db02c9983c76efc6aa56cf0bdc75a5b to your computer and use it in GitHub Desktop.
Kotlin Multiplatform Base64 - no extra dependencies. Based on Java's implementation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. | |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
* | |
* This code is free software; you can redistribute it and/or modify it | |
* under the terms of the GNU General Public License version 2 only, as | |
* published by the Free Software Foundation. Oracle designates this | |
* particular file as subject to the "Classpath" exception as provided | |
* by Oracle in the LICENSE file that accompanied this code. | |
* | |
* This code is distributed in the hope that it will be useful, but WITHOUT | |
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
* version 2 for more details (a copy is included in the LICENSE file that | |
* accompanied this code). | |
* | |
* You should have received a copy of the GNU General Public License version | |
* 2 along with this work; if not, write to the Free Software Foundation, | |
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
* | |
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
* or visit www.oracle.com if you need additional information or have any | |
* questions. | |
*/ | |
import kotlin.math.min | |
/** | |
* This class consists exclusively of static methods for obtaining | |
* encoders and decoders for the Base64 encoding scheme. The | |
* implementation of this class supports the following types of Base64 | |
* as specified in | |
* [RFC 4648](http://www.ietf.org/rfc/rfc4648.txt) and | |
* [RFC 2045](http://www.ietf.org/rfc/rfc2045.txt). | |
* | |
* | |
* * <a id="basic">**Basic**</a> | |
* | |
* Uses "The Base64 Alphabet" as specified in Table 1 of | |
* RFC 4648 and RFC 2045 for encoding and decoding operation. | |
* The encoder does not add any line feed (line separator) | |
* character. The decoder rejects data that contains characters | |
* outside the base64 alphabet. | |
* | |
* * <a id="url">**URL and Filename safe**</a> | |
* | |
* Uses the "URL and Filename safe Base64 Alphabet" as specified | |
* in Table 2 of RFC 4648 for encoding and decoding. The | |
* encoder does not add any line feed (line separator) character. | |
* The decoder rejects data that contains characters outside the | |
* base64 alphabet. | |
* | |
* * <a id="mime">**MIME**</a> | |
* | |
* Uses "The Base64 Alphabet" as specified in Table 1 of | |
* RFC 2045 for encoding and decoding operation. The encoded output | |
* must be represented in lines of no more than 76 characters each | |
* and uses a carriage return `'\r'` followed immediately by | |
* a linefeed `'\n'` as the line separator. No line separator | |
* is added to the end of the encoded output. All line separators | |
* or other characters not found in the base64 alphabet table are | |
* ignored in decoding operation. | |
* | |
* | |
* | |
* Unless otherwise noted, passing a `null` argument to a | |
* method of this class will cause a [ NullPointerException][java.lang.NullPointerException] to be thrown. | |
* | |
* @author Xueming Shen | |
* @since 1.8 | |
*/ | |
object Base64 { | |
fun ByteArray.encodeToBase64(): String { | |
return encoder.encode(this).decodeToString() | |
} | |
fun String.decodeFromBase64(): ByteArray { | |
return decoder.decode(this.encodeToByteArray()) | |
} | |
/** | |
* Returns a [Encoder] that encodes using the | |
* [Basic](#basic) type base64 encoding scheme. | |
* | |
* @return A Base64 encoder. | |
*/ | |
val encoder = Encoder(null, -1, true) | |
/** | |
* Returns a [Decoder] that decodes using the | |
* [Basic](#basic) type base64 encoding scheme. | |
* | |
* @return A Base64 decoder. | |
*/ | |
val decoder = Decoder() | |
/** | |
* This class implements an encoder for encoding byte data using | |
* the Base64 encoding scheme as specified in RFC 4648 and RFC 2045. | |
* | |
* | |
* Instances of [Encoder] class are safe for use by | |
* multiple concurrent threads. | |
* | |
* | |
* Unless otherwise noted, passing a `null` argument to | |
* a method of this class will cause a | |
* [NullPointerException][java.lang.NullPointerException] to | |
* be thrown. | |
* | |
* @see Decoder | |
* | |
* @since 1.8 | |
*/ | |
class Encoder internal constructor(private val newline: ByteArray?, private val linemax: Int, private val doPadding: Boolean) { | |
private fun outLength(srclen: Int): Int { | |
var len = 0 | |
len = if (doPadding) { | |
4 * ((srclen + 2) / 3) | |
} else { | |
val n = srclen % 3 | |
4 * (srclen / 3) + if (n == 0) 0 else n + 1 | |
} | |
if (linemax > 0) // line separators | |
len += (len - 1) / linemax * newline!!.size | |
return len | |
} | |
/** | |
* Encodes all bytes from the specified byte array into a newly-allocated | |
* byte array using the [Base64] encoding scheme. The returned byte | |
* array is of the length of the resulting bytes. | |
* | |
* @param src | |
* the byte array to encode | |
* @return A newly-allocated byte array containing the resulting | |
* encoded bytes. | |
*/ | |
fun encode(src: ByteArray): ByteArray { | |
val len = outLength(src.size) // dst array size | |
val dst = ByteArray(len) | |
val ret = encode0(src, 0, src.size, dst) | |
return if (ret != dst.size) dst.copyOf(ret) else dst | |
} | |
private fun encodeBlock(src: ByteArray, sp: Int, sl: Int, dst: ByteArray, dp: Int) { | |
var sp0 = sp | |
var dp0 = dp | |
while (sp0 < sl) { | |
val bits: Int = src[sp0++].toInt() and 0xff shl 16 or ( | |
src[sp0++].toInt() and 0xff shl 8) or | |
(src[sp0++].toInt() and 0xff) | |
dst[dp0++] = toBase64[bits ushr 18 and 0x3f].toByte() | |
dst[dp0++] = toBase64[bits ushr 12 and 0x3f].toByte() | |
dst[dp0++] = toBase64[bits ushr 6 and 0x3f].toByte() | |
dst[dp0++] = toBase64[bits and 0x3f].toByte() | |
} | |
} | |
private fun encode0(src: ByteArray, off: Int, end: Int, dst: ByteArray): Int { | |
val base64 = toBase64 | |
var sp = off | |
var slen = (end - off) / 3 * 3 | |
val sl = off + slen | |
if (linemax > 0 && slen > linemax / 4 * 3) slen = linemax / 4 * 3 | |
var dp = 0 | |
while (sp < sl) { | |
val sl0: Int = min(sp + slen, sl) | |
encodeBlock(src, sp, sl0, dst, dp) | |
val dlen = (sl0 - sp) / 3 * 4 | |
dp += dlen | |
sp = sl0 | |
if (dlen == linemax && sp < end) { | |
for (b in newline!!) { | |
dst[dp++] = b | |
} | |
} | |
} | |
if (sp < end) { // 1 or 2 leftover bytes | |
val b0: Int = src[sp++].toInt() and 0xff | |
dst[dp++] = base64[b0 shr 2].toByte() | |
if (sp == end) { | |
dst[dp++] = base64[b0 shl 4 and 0x3f].toByte() | |
if (doPadding) { | |
dst[dp++] = '='.toByte() | |
dst[dp++] = '='.toByte() | |
} | |
} else { | |
val b1: Int = src[sp++].toInt() and 0xff | |
dst[dp++] = base64[b0 shl 4 and 0x3f or (b1 shr 4)].toByte() | |
dst[dp++] = base64[b1 shl 2 and 0x3f].toByte() | |
if (doPadding) { | |
dst[dp++] = '='.toByte() | |
} | |
} | |
} | |
return dp | |
} | |
companion object { | |
/** | |
* This array is a lookup table that translates 6-bit positive integer | |
* index values into their "Base64 Alphabet" equivalents as specified | |
* in "Table 1: The Base64 Alphabet" of RFC 2045 (and RFC 4648). | |
*/ | |
val toBase64 = charArrayOf( | |
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', | |
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', | |
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', | |
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', | |
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' | |
) | |
/** | |
* It's the lookup table for "URL and Filename safe Base64" as specified | |
* in Table 2 of the RFC 4648, with the '+' and '/' changed to '-' and | |
* '_'. This table is used when BASE64_URL is specified. | |
*/ | |
internal val toBase64URL = charArrayOf( | |
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', | |
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', | |
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', | |
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', | |
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' | |
) | |
} | |
} | |
/** | |
* This class implements a decoder for decoding byte data using the | |
* Base64 encoding scheme as specified in RFC 4648 and RFC 2045. | |
* | |
* | |
* The Base64 padding character `'='` is accepted and | |
* interpreted as the end of the encoded byte data, but is not | |
* required. So if the final unit of the encoded byte data only has | |
* two or three Base64 characters (without the corresponding padding | |
* character(s) padded), they are decoded as if followed by padding | |
* character(s). If there is a padding character present in the | |
* final unit, the correct number of padding character(s) must be | |
* present, otherwise `IllegalArgumentException` ( | |
* `IOException` when reading from a Base64 stream) is thrown | |
* during decoding. | |
* | |
* | |
* Instances of [Decoder] class are safe for use by | |
* multiple concurrent threads. | |
* | |
* | |
* Unless otherwise noted, passing a `null` argument to | |
* a method of this class will cause a | |
* [NullPointerException][java.lang.NullPointerException] to | |
* be thrown. | |
* | |
* @see Encoder | |
* | |
* @since 1.8 | |
*/ | |
class Decoder { | |
companion object { | |
/** | |
* Lookup table for decoding unicode characters drawn from the | |
* "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into | |
* their 6-bit positive integer equivalents. Characters that | |
* are not in the Base64 alphabet but fall within the bounds of | |
* the array are encoded to -1. | |
* | |
*/ | |
internal val fromBase64 = IntArray(256) | |
/** | |
* Lookup table for decoding "URL and Filename safe Base64 Alphabet" | |
* as specified in Table2 of the RFC 4648. | |
*/ | |
private val fromBase64URL = IntArray(256) | |
init { | |
fromBase64.fill(-1) | |
for (i in Encoder.toBase64.indices) fromBase64[Encoder.toBase64[i].toInt()] = i | |
fromBase64['='.toInt()] = -2 | |
} | |
init { | |
fromBase64URL.fill(-1) | |
for (i in Encoder.toBase64URL.indices) fromBase64URL[Encoder.toBase64URL[i].toInt()] = i | |
fromBase64URL['='.toInt()] = -2 | |
} | |
} | |
/** | |
* Decodes all bytes from the input byte array using the [Base64] | |
* encoding scheme, writing the results into a newly-allocated output | |
* byte array. The returned byte array is of the length of the resulting | |
* bytes. | |
* | |
* @param src | |
* the byte array to decode | |
* | |
* @return A newly-allocated byte array containing the decoded bytes. | |
* | |
* @throws IllegalArgumentException | |
* if `src` is not in valid Base64 scheme | |
*/ | |
fun decode(src: ByteArray): ByteArray { | |
var dst = ByteArray(outLength(src, 0, src.size)) | |
val ret = decode0(src, 0, src.size, dst) | |
if (ret != dst.size) { | |
dst = dst.copyOf(ret) | |
} | |
return dst | |
} | |
private fun outLength(src: ByteArray, sp: Int, sl: Int): Int { | |
var sp = sp | |
var paddings = 0 | |
var len = sl - sp | |
if (len == 0) return 0 | |
if (len < 2) { | |
throw IllegalArgumentException( | |
"Input byte[] should at least have 2 bytes for base64 bytes" | |
) | |
} | |
if (src[sl - 1].toChar() == '=') { | |
paddings++ | |
if (src[sl - 2].toChar() == '=') paddings++ | |
} | |
if (paddings == 0 && len and 0x3 != 0) paddings = 4 - (len and 0x3) | |
return 3 * ((len + 3) / 4) - paddings | |
} | |
private fun decode0(src: ByteArray, sp: Int, sl: Int, dst: ByteArray): Int { | |
var sp = sp | |
val base64 = if (false) fromBase64URL else fromBase64 | |
var dp = 0 | |
var bits = 0 | |
var shiftto = 18 // pos of first byte of 4-byte atom | |
while (sp < sl) { | |
if (shiftto == 18 && sp + 4 < sl) { // fast path | |
val sl0 = sp + (sl - sp and 3.inv()) | |
while (sp < sl0) { | |
val b1 = base64[src[sp++].toInt() and 0xff] | |
val b2 = base64[src[sp++].toInt() and 0xff] | |
val b3 = base64[src[sp++].toInt() and 0xff] | |
val b4 = base64[src[sp++].toInt() and 0xff] | |
if (b1 or b2 or b3 or b4 < 0) { // non base64 byte | |
sp -= 4 | |
break | |
} | |
val bits0 = b1 shl 18 or (b2 shl 12) or (b3 shl 6) or b4 | |
dst[dp++] = (bits0 shr 16).toByte() | |
dst[dp++] = (bits0 shr 8).toByte() | |
dst[dp++] = bits0.toByte() | |
} | |
if (sp >= sl) break | |
} | |
var b: Int = src[sp++].toInt() and 0xff | |
if (base64[b].also { b = it } < 0) { | |
if (b == -2) { // padding byte '=' | |
// = shiftto==18 unnecessary padding | |
// x= shiftto==12 a dangling single x | |
// x to be handled together with non-padding case | |
// xx= shiftto==6&&sp==sl missing last = | |
// xx=y shiftto==6 last is not = | |
require( | |
!(shiftto == 6 && (sp == sl || src[sp++].toChar() != '=') || | |
shiftto == 18) | |
) { "Input byte array has wrong 4-byte ending unit" } | |
break | |
} | |
throw IllegalArgumentException("Illegal base64 character " + src[sp - 1].toInt().toString(16)) | |
} | |
bits = bits or (b shl shiftto) | |
shiftto -= 6 | |
if (shiftto < 0) { | |
dst[dp++] = (bits shr 16).toByte() | |
dst[dp++] = (bits shr 8).toByte() | |
dst[dp++] = bits.toByte() | |
shiftto = 18 | |
bits = 0 | |
} | |
} | |
// reached end of byte array or hit padding '=' characters. | |
when (shiftto) { | |
6 -> { | |
dst[dp++] = (bits shr 16).toByte() | |
} | |
0 -> { | |
dst[dp++] = (bits shr 16).toByte() | |
dst[dp++] = (bits shr 8).toByte() | |
} | |
else -> require(shiftto != 12) { | |
// dangling single "x", incorrectly encoded. | |
"Last unit does not have enough valid bits" | |
} | |
} | |
// anything left is invalid, if is not MIME. | |
// if MIME, ignore all non-base64 character | |
while (sp < sl) { | |
throw IllegalArgumentException( | |
"Input byte array has incorrect ending byte at $sp" | |
) | |
} | |
return dp | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.junit.jupiter.api.Assertions.assertArrayEquals | |
import org.junit.jupiter.api.Test | |
import kotlin.random.Random.Default.nextBytes | |
import kotlin.test.assertEquals | |
/** | |
* This JVM specific test can verifiy the common Base64 implemenation | |
*/ | |
class Base64Test { | |
@Test | |
fun encodeDecode() { | |
(0..100).forEach { i -> | |
val input = nextBytes(i * 10) | |
val javaEncoded = java.util.Base64.getEncoder().encodeToString(input) | |
val kotlinEncoded = String(Base64.encoder.encode(input)) | |
assertEquals(javaEncoded, kotlinEncoded) | |
assertArrayEquals(input, Base64.decoder.decode(kotlinEncoded.encodeToByteArray())) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you