Закодировать и декодировать строку
Base64.encode принимает ByteArray и возвращает String. encodeToByteArray() и decodeToString() работают с UTF-8 в обе стороны:
import kotlin.io.encoding.Base64
val enc = Base64.encode("héllo wörld".encodeToByteArray())
println(enc) // aMOpbGxvIHfDtnJsZA==
println(Base64.decode(enc).decodeToString()) // héllo wörld
Сам Base64 – это стандартный экземпляр (Base64.Default). Есть также Base64.UrlSafe и Base64.Mime.
В Kotlin старше 2.2 этому API нужен @OptIn(ExperimentalEncodingApi::class). Если обновиться нельзя, java.util.Base64 работает так же, как на странице Base64 в Java:
java.util.Base64.getEncoder().encodeToString("hi".toByteArray()) // aGk=
Типичные применения – HTTP-заголовки и data URI:
val token = Base64.encode("user:pass".encodeToByteArray())
println("Authorization: Basic $token") // Authorization: Basic dXNlcjpwYXNz
val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47)
println("data:image/png;base64," + Base64.encode(png)) // data:image/png;base64,iVBORw==
URL-safe Base64 и параметры дополнения
Base64.UrlSafe меняет +/ на -_. Дополнением управляет withPadding, который возвращает новый настроенный экземпляр:
val d = "<<???>>".encodeToByteArray()
println(Base64.UrlSafe.encode(d)) // PDw_Pz8-Pg==
val urlNoPad = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)
println(urlNoPad.encode(d)) // PDw_Pz8-Pg
println(urlNoPad.decode("PDw_Pz8-Pg").decodeToString()) // <<???>>
Четыре варианта:
| Вариант | Кодирование | Декодирование |
|---|---|---|
PRESENT (по умолчанию) | пишет = | требует = |
ABSENT | без = | отвергает = |
PRESENT_OPTIONAL | пишет = | принимает оба варианта |
ABSENT_OPTIONAL | без = | принимает оба варианта |
Экземпляры по умолчанию тут строгие:
Base64.UrlSafe.decode("PDw_Pz8-Pg")
// IllegalArgumentException: The padding option is set to PRESENT,
// but the input is not properly padded
Для частей JWT и других токенов обычно подходят ABSENT_OPTIONAL или PRESENT_OPTIONAL: те, кто их выпускает, по-разному относятся к дополнению. Подробности в статьях Base64 и Base64URL и про дополнение.
Например, так читается полезная нагрузка JWT:
val jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
val url = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
println(url.decode(jwt.split(".")[1]).decodeToString()) // {"sub":"1234"}
Перенос строк MIME
Base64.Mime пишет строки по 76 символов, разделённые \r\n, а при декодировании игнорирует разделители строк:
Base64.Mime.encode("x".repeat(60).encodeToByteArray())
// "eHh4eHh4...eHh4" (76 chars) + "\r\n" + "eHh4"
Base64.Mime.decode("aGVs\r\nbG8K").decodeToString() // "hello\n"
Кодировщик MIME Base64 показывает такую же разметку в браузере.
Закодировать и декодировать файл
Небольшие файлы помещаются в память:
import java.io.File
println(Base64.encode(File("hello.txt").readBytes())) // aGVsbG8K
File("copy.txt").writeBytes(Base64.decode("aGVsbG8K"))
Большие файлы потоком
На JVM потоки оборачивают encodingWith и decodingWith. В Kotlin 2.4 они всё ещё экспериментальные, так что им нужны opt-in и отдельные импорты:
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
import kotlin.io.encoding.encodingWith
import java.io.File
@OptIn(ExperimentalEncodingApi::class)
fun encodeFile(src: File, dst: File) {
dst.outputStream().encodingWith(Base64).use { out ->
src.inputStream().use { it.copyTo(out) }
}
}
@OptIn(ExperimentalEncodingApi::class)
fun decodeFile(src: File, dst: File) {
src.inputStream().decodingWith(Base64).use { inp ->
dst.outputStream().use { inp.copyTo(it) }
}
}
Блок use важен: при закрытии кодирующего потока записываются последняя группа и дополнение. Мы проверили это на файле в 300 КБ: потоковый вывод совпал с Base64.encode(readBytes()), а после обратного декодирования получился исходный файл байт в байт. Если не хочется экспериментального API, ту же работу делает java.util.Base64.getEncoder().wrap(outputStream).
Проверка и тексты исключений
Декодер Kotlin строже, чем в Java. Он отвергает ненулевые биты дополнения, которые Java и большинство других библиотек принимают. Любой сбой – это IllegalArgumentException:
Вход для Base64.decode | Сообщение |
|---|---|
PDw_Pz8-Pg== | Invalid symbol '_'(137) at index 3 |
"aGVs\nbG8K" | Invalid symbol '\n'(12) at index 4 |
aGk=!! | Symbol '!'(41) at index 3 is prohibited after the pad character |
aGk== | Symbol '='(75) at index 3 is prohibited after the pad character |
aGl= | The pad bits must be zeros |
a | Input should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1 |
Число в скобках – код символа в восьмеричной системе (137 – это _, 12 – перевод строки). Функция проверки умещается в одну строку:
fun isBase64(s: String) = runCatching { Base64.decode(s) }.isSuccess
Чтобы увидеть, где именно ломается конкретная строка, откройте валидатор Base64; обычные причины перечислены в статье об ошибках Base64.
Подводные камни
- Строки без дополнения и экземпляр по умолчанию. У сегментов JWT и многих токенов нет
=. НастройтеPRESENT_OPTIONALилиABSENT_OPTIONAL, тогда дописывать дополнение вручную не придётся. - Флаг
DEFAULTв Android.android.util.Base64.encodeToString(bytes, Base64.DEFAULT)вставляет перевод строки через каждые 76 символов и в конце. В HTTP-заголовке это ломает запрос. ИспользуйтеNO_WRAP, а для токенов –NO_WRAP or URL_SAFE or NO_PADDING. - Кодировка
String.toByteArray(). В Kotlin по умолчанию это UTF-8, в отличие отgetBytes()в Java на старых JDK, так что"héllo".toByteArray()безопасен. - Декодирование двоичных данных в строку.
decodeToString()на байтах картинки заменяет некорректные последовательности наU+FFFDи портит данные. ОставляйтеByteArray.
Версии
- Kotlin 1.8.20:
kotlin.io.encoding.Base64добавлен как экспериментальный. - Kotlin 2.0.20:
withPaddingиPaddingOption. - Kotlin 2.2: основной API стал стабильным; потоковые помощники для JVM остаются экспериментальными.
Вопросы и ответы
Нужен ли ещё @OptIn(ExperimentalEncodingApi::class)?
Для Base64.encode и Base64.decode в Kotlin 2.2 и новее – нет. Потоковым помощникам encodingWith и decodingWith в Kotlin 2.4 opt-in всё ещё нужен.
Как в Kotlin декодировать Base64 без дополнения?
Используйте настроенный экземпляр: Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT) для строк без дополнения или PRESENT_OPTIONAL, чтобы принимать оба варианта. Экземпляры по умолчанию требуют дополнения.
Что брать: android.util.Base64 или kotlin.io.encoding.Base64?
В новом коде API Kotlin работает на любой версии Android, потому что входит в стандартную библиотеку. android.util.Base64 добавляет переносы строк, если не передать Base64.NO_WRAP, и это частая причина сломанных заголовков.