Skip to content

A Kotlin Multiplatform library for configurable, streamable, efficient and extensible Encoding/Decoding with support for base16/32/64.

License

Notifications You must be signed in to change notification settings

05nelsonm/encoding

Repository files navigation

encoding

badge-license badge-latest

badge-kotlin

badge-platform-android badge-platform-jvm badge-platform-js badge-platform-js-node badge-platform-wasm badge-platform-linux badge-platform-macos badge-platform-ios badge-platform-tvos badge-platform-watchos badge-platform-windows badge-support-android-native badge-support-apple-silicon badge-support-js-ir badge-support-linux-arm

Configurable, streamable, efficient and extensible Encoding/Decoding for Kotlin Multiplatform.

API docs available at https://encoding.matthewnelson.io

Base16 (a.k.a. "hex")

Base32

Base64

Usage

Configure EncoderDecoder(s) to your needs

val base16 = Base16 {
    // Ignore whitespace and new lines when decoding
    isLenient = true

    // Insert line breaks every X characters of encoded output
    lineBreakInterval = 10

    // Use lowercase instead of uppercase characters when encoding
    encodeToLowercase = true
}

// Shortcuts
val base16StrictSettings = Base16(strict = true)
val base16DefaultSettings = Base16()

// Alternatively, use the static instance with its default settings
Base16
val base32Crockford = Base32Crockford {
    isLenient = true
    encodeToLowercase = false

    // Insert hyphens every X characters of encoded output
    hyphenInterval = 5

    // Optional data integrity check unique to the Crockford spec
    checkSymbol('*')

    // Only apply the checkSymbol & reset hyphen interval counter
    // when Encoder.Feed.doFinal is called (see builder docs for
    // more info) 
    finalizeWhenFlushed = false
}

// Alternatively, use the static instance with its default settings
Base32.Crockford

val base32Default = Base32Default {
    isLenient = true
    lineBreakInterval = 64
    encodeToLowercase = true
    
    // Skip padding of the encoded output
    padEncoded = false
}

// Alternatively, use the static instance with its default settings
Base32.Default

val base32Hex = Base32Hex {
    isLenient = true
    lineBreakInterval = 64
    encodeToLowercase = false
    padEncoded = true
}

// Alternatively, use the static instance with its default settings
Base32.Hex
// NOTE: Base64 can _decode_ both Default and UrlSafe, no matter what
// encodeToUrlSafe is set to.
val base64 = Base64 {
    isLenient = true
    lineBreakInterval = 64
    encodeToUrlSafe = false
    padEncoded = true
}

// Alternatively, use the static instance with its default settings
Base64.Default

// Inherit settings from another EncoderDecoder's Config
val base64UrlSafe = Base64(base64.config) {
    encodeToUrlSafe = true
    padEncoded = false
}

// Alternatively, use the static instance with its default settings
Base64.UrlSafe

Encoding/Decoding Extension Functions

val text = "Hello World!"
val bytes = text.encodeToByteArray()

// Choose the output type that suits your needs
// without having to perform unnecessary intermediate
// transformations (can be useful for security 
// purposes, too, as you are able to clear Arrays
// before they are de-referenced).
val encodedString = bytes.encodeToString(Base64.Default)
val encodedChars = bytes.encodeToCharArray(Base32.Default)

val decodedString = try {
    encodedString.decodeToByteArray(Base64.Default)
} catch (e: EncodingException) {
    Log.e("Something went terribly wrong", e)
    null
}
// Swallow `EncodingException`s by using the `*OrNull` variants
val decodedChars = encodedChars.decodeToByteArrayOrNull(Base32.Default)

Encoding/Decoding Feed(s) (i.e. Streaming)

Feed's are a new concept which enable some pretty awesome things. They break the encoding/decoding process into its individual parts, such that the medium for which data is coming from or going to can be anything; Feed's only care about Byte(s) and Char(s)!

// e.g. Concatenate multiple encodings
val sb = StringBuilder()

// Use our own line break out feed in order to add a delimiter between
// encodings and preserve the counter.
val out = LineBreakOutFeed(interval = 64) { char -> sb.append(char) }

Base64.Default.newEncoderFeed(out).use { feed ->
    "Hello World 1!".encodeToByteArray().forEach { b -> feed.consume(b)  }
    feed.flush()
    out.output('.')
    "Hello World 2!".encodeToByteArray().forEach { b -> feed.consume(b)  }
}

println(sb.toString())
// SGVsbG8gV29ybGQgMSE=.SGVsbG8gV29ybGQgMiE=
// e.g. Writing encoded data to a File in Java.
// NOTE: try/catch omitted for this example.

file.outputStream().use { oStream ->
    Base64.Default.newEncoderFeed { encodedChar ->
        // As encoded data comes out of the feed,
        // write it to the file.
        oStream.write(encodedChar.code)
    }.use { feed ->

        // Push data through the feed.
        //
        // There are NO size/length limitations with `Feed`s.
        // You are only limited by the medium you use to store
        // the output (e.g. the maximum size of a ByteArray is
        // Int.MAX_VALUE).
        //
        // The `Feed.use` extension function calls `doFinal`
        // automatically, which closes the `Encoder.Feed`
        // and performs finalization of the operation (such as
        // adding padding).
        "Hello World!".encodeToByteArray().forEach { b ->
            feed.consume(b)
        }
    }
}

As Feed(s) is a new concept, they can be "bulky" to use (as you will see in the example below). This is due to a lack of extension functions for them, but it's something I hope can be built out over time with your help (PRs and FeatureRequests are always welcome)!

// e.g. Reading encoded data from a File in Java.
// NOTE: try/catch omitted for this example.

// Pre-calculate the output size for the given encoding
// spec; in this case, Base64.
val size = Base64.Default.config.decodeOutMaxSize(file.length())

// Since we will be storing the data in a StringBuilder,
// we need to check if the output size would exceed
// StringBuilder's maximum capacity.
if (size > Int.MAX_VALUE.toLong()) {
    // Alternatively, one could fall back to chunking, but that
    // is beyond the scope of this example.
    throw EncodingSizeException(
        "File contents would be too large after decoding to store in a StringBuilder"
    )
}

val sb = StringBuilder(size.toInt())

file.inputStream().reader().use { iStreamReader ->
    Base64.Default.newDecoderFeed { decodedByte ->
        // As decoded data comes out of the feed,
        // update the StringBuilder.
        sb.append(decodedByte.toInt().toChar())
    }.use { feed ->

        val buffer = CharArray(4096)
        while (true) {
            val read = iStreamReader.read(buffer)
            if (read == -1) break
            
            // Push encoded data from the file through the feed.
            //
            // The `Feed.use` extension function calls `doFinal`
            // automatically, which closes the `Decoder.Feed`
            // and performs finalization of the operation.
            for (i in 0 until read) {
                feed.consume(buffer[i])
            }
        }
    }
}

println(sb.toString())

Alternatively, create your own EncoderDecoder(s) using the abstractions provided by encoding-core

Sample

See sample project

Get Started

// build.gradle.kts
dependencies {
    val encoding = "2.3.1"
    implementation("io.matthewnelson.encoding:base16:$encoding")
    implementation("io.matthewnelson.encoding:base32:$encoding")
    implementation("io.matthewnelson.encoding:base64:$encoding")

    // Only necessary if you just want the abstractions to create your own EncoderDecoder(s)
    implementation("io.matthewnelson.encoding:core:$encoding")
}

Alternatively, you can use the BOM.

// build.gradle.kts
dependencies {
    // define the BOM and its version
    implementation(platform("io.matthewnelson.encoding:bom:2.3.1"))

    // define artifacts without version
    implementation("io.matthewnelson.encoding:base16")
    implementation("io.matthewnelson.encoding:base32")
    implementation("io.matthewnelson.encoding:base64")

    // Only necessary if you just want the abstractions to create your own EncoderDecoder(s)
    implementation("io.matthewnelson.encoding:core")
}