Add encode and decode padding options
diff --git a/libraries/stdlib/api/js/kotlin.io.encoding.kt b/libraries/stdlib/api/js/kotlin.io.encoding.kt index 8241b5b..51af4ed 100644 --- a/libraries/stdlib/api/js/kotlin.io.encoding.kt +++ b/libraries/stdlib/api/js/kotlin.io.encoding.kt
@@ -17,11 +17,29 @@ public final fun encodeToByteArray(source: kotlin.ByteArray, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.ByteArray + public final fun withPadding(onEncode: kotlin.io.encoding.Base64.EncodePadding = ..., onDecode: kotlin.io.encoding.Base64.DecodePadding = ...): kotlin.io.encoding.Base64 + + public final fun withPaddingOptions(encodeOption: kotlin.io.encoding.Base64.EncodePadding = ..., decodeOption: kotlin.io.encoding.Base64.DecodePadding = ...): kotlin.io.encoding.Base64 + + public final enum class DecodePadding : kotlin.Enum<kotlin.io.encoding.Base64.DecodePadding> { + enum entry REQUIRE_PRESENT + + enum entry REQUIRE_ABSENT + + enum entry ALLOW_BOTH + } + public companion object of Base64 Default : kotlin.io.encoding.Base64 { public final val Mime: kotlin.io.encoding.Base64 { get; } public final val UrlSafe: kotlin.io.encoding.Base64 { get; } } + + public final enum class EncodePadding : kotlin.Enum<kotlin.io.encoding.Base64.EncodePadding> { + enum entry WRITE + + enum entry OMIT + } } @kotlin.RequiresOptIn(level = Level.ERROR)
diff --git a/libraries/stdlib/src/kotlin/io/encoding/Base64.kt b/libraries/stdlib/src/kotlin/io/encoding/Base64.kt index 8f94169..4283db6 100644 --- a/libraries/stdlib/src/kotlin/io/encoding/Base64.kt +++ b/libraries/stdlib/src/kotlin/io/encoding/Base64.kt
@@ -19,12 +19,51 @@ @ExperimentalEncodingApi public open class Base64 private constructor( internal val isUrlSafe: Boolean, - internal val isMimeScheme: Boolean + internal val isMimeScheme: Boolean, + private val encodePadding: EncodePadding = EncodePadding.WRITE, + private val decodePadding: DecodePadding = DecodePadding.REQUIRE_PRESENT ) { init { require(!isUrlSafe || !isMimeScheme) } + public enum class EncodePadding { + WRITE, + OMIT + } + + public enum class DecodePadding { + REQUIRE_PRESENT, + REQUIRE_ABSENT, + ALLOW_BOTH + } + + // TODO: Having default values can implicitly change the option that was not specified. + // Should the default value be null? + public fun withPadding( + onEncode: EncodePadding = EncodePadding.WRITE, + onDecode: DecodePadding = DecodePadding.REQUIRE_PRESENT + ): Base64 { + return if (onEncode != encodePadding || onDecode != decodePadding) { + Base64(isUrlSafe, isMimeScheme, onEncode, onDecode) + } else { + this + } + } + + // TODO: Having default values can implicitly change the option that was not specified. + // Should the default value be null? + public fun withPaddingOptions( + encodeOption: EncodePadding = EncodePadding.WRITE, + decodeOption: DecodePadding = DecodePadding.REQUIRE_PRESENT + ): Base64 { + return if (encodeOption != encodePadding || decodeOption != decodePadding) { + Base64(isUrlSafe, isMimeScheme, encodeOption, decodeOption) + } else { + this + } + } + /** * Encodes bytes from the specified [source] array or its subrange. * Returns a [ByteArray] containing the resulting symbols. @@ -301,8 +340,10 @@ val bits = byte1 shl 4 destination[destinationIndex++] = encodeMap[bits ushr 6] destination[destinationIndex++] = encodeMap[bits and 0x3F] - destination[destinationIndex++] = padSymbol - destination[destinationIndex++] = padSymbol + if (encodePadding == EncodePadding.WRITE) { + destination[destinationIndex++] = padSymbol + destination[destinationIndex++] = padSymbol + } } 2 -> { val byte1 = source[sourceIndex++].toInt() and 0xFF @@ -311,7 +352,9 @@ destination[destinationIndex++] = encodeMap[bits ushr 12] destination[destinationIndex++] = encodeMap[(bits ushr 6) and 0x3F] destination[destinationIndex++] = encodeMap[bits and 0x3F] - destination[destinationIndex++] = padSymbol + if (encodePadding == EncodePadding.WRITE) { + destination[destinationIndex++] = padSymbol + } } } @@ -324,7 +367,10 @@ // includes padding chars val groups = (sourceSize + bytesPerGroup - 1) / bytesPerGroup val lineSeparators = if (isMimeScheme) (groups - 1) / mimeGroupsPerLine else 0 - val size = groups * symbolsPerGroup + lineSeparators * 2 + var size = groups * symbolsPerGroup + lineSeparators * 2 + if (encodePadding == EncodePadding.OMIT) { + size -= (groups * bytesPerGroup - sourceSize) // 0, 1, or 2 + } if (size < 0) { // Int overflow throw IllegalArgumentException("Input is too big") } @@ -394,8 +440,12 @@ if (byteStart == -bitsPerByte + bitsPerSymbol) { // dangling single symbol, incorrectly encoded throw IllegalArgumentException("The last unit of input does not have enough bits") } - if (byteStart != -bitsPerByte && !hasPadding) { - throw IllegalArgumentException("The input should be padded") + if (byteStart != -bitsPerByte) { + if (decodePadding == DecodePadding.REQUIRE_PRESENT && !hasPadding || + decodePadding == DecodePadding.REQUIRE_ABSENT && hasPadding + ) { + throw IllegalArgumentException("The input should be padded") + } } if (payload != 0) { // the pad bits are non-zero throw IllegalArgumentException("The pad bits should be zeros")
diff --git a/libraries/stdlib/test/io.encoding/Base64Test.kt b/libraries/stdlib/test/io.encoding/Base64Test.kt index 8696425..d82ac70 100644 --- a/libraries/stdlib/test/io.encoding/Base64Test.kt +++ b/libraries/stdlib/test/io.encoding/Base64Test.kt
@@ -178,6 +178,23 @@ // paddings are required assertFailsWith<IllegalArgumentException>(scheme) { codec.decode("Zg") } assertFailsWith<IllegalArgumentException>(scheme) { codec.decode("Zm9vYmE") } + + // paddings are prohibited + val noPaddingCodec = codec.withPadding(onDecode = Base64.DecodePadding.REQUIRE_ABSENT) + assertFailsWith<IllegalArgumentException>(scheme) { noPaddingCodec.decode("Zg==") } + assertFailsWith<IllegalArgumentException>(scheme) { noPaddingCodec.decode("Zm9vYmE=") } + + // paddings are allowed but not required + val allowPaddingCodec = codec.withPaddingOptions(decodeOption = Base64.DecodePadding.ALLOW_BOTH) + assertContentEquals("f".encodeToByteArray(), allowPaddingCodec.decode("Zg"), message = scheme) + assertContentEquals("fooba".encodeToByteArray(), allowPaddingCodec.decode("Zm9vYmE"), message = scheme) + assertContentEquals("f".encodeToByteArray(), allowPaddingCodec.decode("Zg=="), message = scheme) + assertContentEquals("fooba".encodeToByteArray(), allowPaddingCodec.decode("Zm9vYmE="), message = scheme) + + // encode with no padding + val withoutPaddingBase64 = codec.withPadding(onEncode = Base64.EncodePadding.OMIT) + assertEquals("Zg", withoutPaddingBase64.encode("f".encodeToByteArray())) + assertEquals("Zm9vYmE", withoutPaddingBase64.encode("fooba".encodeToByteArray())) } } @@ -272,4 +289,29 @@ val expected = "Zm9vYmFy".repeat(76).chunked(76).joinToString(separator = "\r\n") testEncode(Base64.Mime, "foobar".repeat(76).encodeToByteArray(), expected) } +} + +private fun f() { + val binary = "foobar".encodeToByteArray() + val text = "Zm9vYmFy".encodeToByteArray() + + // Encode with padding + Base64.encode(binary) + Base64.Mime.encode(binary) + + // Encode without padding + Base64.withPadding(onEncode = Base64.EncodePadding.WRITE).encode(binary) + Base64.UrlSafe.withPadding(onEncode = Base64.EncodePadding.WRITE).encode(binary) + + // Decode with required padding + Base64.withPadding(onDecode = Base64.DecodePadding.REQUIRE_PRESENT).decode(text) + Base64.Mime.withPadding(onDecode = Base64.DecodePadding.REQUIRE_PRESENT).decode(text) + + // Decode with required no padding + Base64.withPadding(onDecode = Base64.DecodePadding.REQUIRE_ABSENT).decode(text) + Base64.Mime.withPadding(onDecode = Base64.DecodePadding.REQUIRE_ABSENT).decode(text) + + // Decode with required padding or no padding + Base64.withPadding(onDecode = Base64.DecodePadding.ALLOW_BOTH).decode(text) + Base64.Mime.withPadding(onDecode = Base64.DecodePadding.ALLOW_BOTH).decode(text) } \ No newline at end of file
diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 94f966a9..d706bd7 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt
@@ -3279,7 +3279,6 @@ public class kotlin/io/encoding/Base64 { public static final field Default Lkotlin/io/encoding/Base64$Default; - public synthetic fun <init> (ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun decode (Ljava/lang/CharSequence;II)[B public final fun decode ([BII)[B public static synthetic fun decode$default (Lkotlin/io/encoding/Base64;Ljava/lang/CharSequence;IIILjava/lang/Object;)[B @@ -3296,6 +3295,19 @@ public static synthetic fun encodeToAppendable$default (Lkotlin/io/encoding/Base64;[BLjava/lang/Appendable;IIILjava/lang/Object;)Ljava/lang/Appendable; public final fun encodeToByteArray ([BII)[B public static synthetic fun encodeToByteArray$default (Lkotlin/io/encoding/Base64;[BIIILjava/lang/Object;)[B + public final fun withPadding (Lkotlin/io/encoding/Base64$EncodePadding;Lkotlin/io/encoding/Base64$DecodePadding;)Lkotlin/io/encoding/Base64; + public static synthetic fun withPadding$default (Lkotlin/io/encoding/Base64;Lkotlin/io/encoding/Base64$EncodePadding;Lkotlin/io/encoding/Base64$DecodePadding;ILjava/lang/Object;)Lkotlin/io/encoding/Base64; + public final fun withPaddingOptions (Lkotlin/io/encoding/Base64$EncodePadding;Lkotlin/io/encoding/Base64$DecodePadding;)Lkotlin/io/encoding/Base64; + public static synthetic fun withPaddingOptions$default (Lkotlin/io/encoding/Base64;Lkotlin/io/encoding/Base64$EncodePadding;Lkotlin/io/encoding/Base64$DecodePadding;ILjava/lang/Object;)Lkotlin/io/encoding/Base64; +} + +public final class kotlin/io/encoding/Base64$DecodePadding : java/lang/Enum { + public static final field ALLOW_BOTH Lkotlin/io/encoding/Base64$DecodePadding; + public static final field REQUIRE_ABSENT Lkotlin/io/encoding/Base64$DecodePadding; + public static final field REQUIRE_PRESENT Lkotlin/io/encoding/Base64$DecodePadding; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lkotlin/io/encoding/Base64$DecodePadding; + public static fun values ()[Lkotlin/io/encoding/Base64$DecodePadding; } public final class kotlin/io/encoding/Base64$Default : kotlin/io/encoding/Base64 { @@ -3303,6 +3315,14 @@ public final fun getUrlSafe ()Lkotlin/io/encoding/Base64; } +public final class kotlin/io/encoding/Base64$EncodePadding : java/lang/Enum { + public static final field OMIT Lkotlin/io/encoding/Base64$EncodePadding; + public static final field WRITE Lkotlin/io/encoding/Base64$EncodePadding; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lkotlin/io/encoding/Base64$EncodePadding; + public static fun values ()[Lkotlin/io/encoding/Base64$EncodePadding; +} + public abstract interface annotation class kotlin/io/encoding/ExperimentalEncodingApi : java/lang/annotation/Annotation { }