A modern I/O library for Android, Java, and Kotlin Multiplatform
—
This document covers Okio's data compression capabilities, including GZIP and deflate algorithms with native zlib integration, as well as ZIP file system access.
GZIP is a widely-used compression format that combines deflate compression with headers and checksums.
class GzipSink(sink: Sink) : Sink {
val deflater: Deflater
override fun write(source: Buffer, byteCount: Long)
override fun flush()
override fun timeout(): Timeout
override fun close()
}
// Extension function for easy creation
fun Sink.gzip(): GzipSinkclass GzipSource(source: Source) : Source {
override fun read(sink: Buffer, byteCount: Long): Long
override fun timeout(): Timeout
override fun close()
}
// Extension function for easy creation
fun Source.gzip(): GzipSource// Compressing data with GZIP
val originalData = "This is some text that will be compressed using GZIP compression algorithm."
val buffer = Buffer()
// Create GZIP compressed data
buffer.gzip().use { gzipSink ->
gzipSink.writeUtf8(originalData)
}
val compressedData = buffer.readByteString()
println("Original size: ${originalData.length}")
println("Compressed size: ${compressedData.size}")
println("Compression ratio: ${compressedData.size.toFloat() / originalData.length}")
// Decompressing GZIP data
val decompressedBuffer = Buffer()
Buffer().write(compressedData).gzip().use { gzipSource ->
decompressedBuffer.writeAll(gzipSource)
}
val decompressedText = decompressedBuffer.readUtf8()
println("Decompressed: '$decompressedText'")
println("Match: ${originalData == decompressedText}")val fs = FileSystem.SYSTEM
val sourceFile = "/tmp/large-file.txt".toPath()
val compressedFile = "/tmp/large-file.txt.gz".toPath()
// Create a large text file
fs.write(sourceFile) {
repeat(1000) { i ->
writeUtf8("Line $i: This is some repeated text to demonstrate compression.\n")
}
}
// Compress file using GZIP
fs.write(compressedFile) {
gzip().use { gzipSink ->
fs.read(sourceFile) {
writeAll(this)
}
}
}
// Compare file sizes
val originalSize = fs.metadata(sourceFile).size ?: 0
val compressedSize = fs.metadata(compressedFile).size ?: 0
println("Original: $originalSize bytes")
println("Compressed: $compressedSize bytes")
println("Saved: ${originalSize - compressedSize} bytes (${(1.0 - compressedSize.toDouble() / originalSize) * 100}%)")
// Decompress and verify
val decompressedContent = fs.read(compressedFile) {
gzip().buffer().readUtf8()
}
val originalContent = fs.read(sourceFile) { readUtf8() }
println("Content matches: ${originalContent == decompressedContent}")Deflate is the core compression algorithm used by GZIP and ZIP formats. Okio provides native deflate support using system zlib libraries.
actual class Deflater {
// Constructors
constructor() // Default compression
constructor(level: Int, nowrap: Boolean) // Custom level and format
// Properties
var flush: Int // Flush mode constants
// Methods
fun getBytesRead(): Long // Total input bytes processed
fun end() // End deflation and release resources
companion object {
// Compression levels
const val NO_COMPRESSION: Int = 0
const val BEST_SPEED: Int = 1
const val BEST_COMPRESSION: Int = 9
const val DEFAULT_COMPRESSION: Int = -1
// Flush modes
const val NO_FLUSH: Int = 0
const val SYNC_FLUSH: Int = 2
const val FULL_FLUSH: Int = 3
const val FINISH: Int = 4
}
}class DeflaterSink(sink: Sink, deflater: Deflater) : Sink {
override fun write(source: Buffer, byteCount: Long)
override fun flush()
override fun timeout(): Timeout
override fun close()
fun finishDeflate() // Finish deflation without closing
}actual class Inflater {
constructor()
constructor(nowrap: Boolean)
fun getBytesRead(): Long
fun getBytesWritten(): Long
fun end()
}
class InflaterSource(source: Source, inflater: Inflater) : Source {
override fun read(sink: Buffer, byteCount: Long): Long
override fun timeout(): Timeout
override fun close()
}// Custom deflate compression with different levels
val testData = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".repeat(100)
fun compressWithLevel(data: String, level: Int): ByteString {
val buffer = Buffer()
val deflater = Deflater(level, nowrap = false)
DeflaterSink(buffer, deflater).use { sink ->
sink.writeUtf8(data)
}
return buffer.readByteString()
}
// Compare compression levels
val levels = listOf(
Deflater.NO_COMPRESSION,
Deflater.BEST_SPEED,
Deflater.DEFAULT_COMPRESSION,
Deflater.BEST_COMPRESSION
)
levels.forEach { level ->
val compressed = compressWithLevel(testData, level)
val ratio = compressed.size.toFloat() / testData.length
println("Level $level: ${compressed.size} bytes (${ratio * 100}%)")
}
// Manual deflate/inflate cycle
val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, nowrap = true)
val inflater = Inflater(nowrap = true)
val originalBuffer = Buffer().writeUtf8("Data to compress")
val compressedBuffer = Buffer()
val decompressedBuffer = Buffer()
// Compress
DeflaterSink(compressedBuffer, deflater).use { sink ->
sink.writeAll(originalBuffer)
}
// Decompress
InflaterSource(compressedBuffer, inflater).use { source ->
decompressedBuffer.writeAll(source)
}
println("Original: ${originalBuffer.size} bytes")
println("Compressed: ${compressedBuffer.size} bytes")
println("Decompressed: ${decompressedBuffer.readUtf8()}")Okio provides read-only access to ZIP files through a FileSystem implementation.
class ZipFileSystem(
zipPath: Path,
fileSystem: FileSystem = FileSystem.SYSTEM,
comment: String? = null
) : FileSystem {
val comment: String?
// Implements all FileSystem methods for ZIP file contents
override fun canonicalize(path: Path): Path
override fun metadataOrNull(path: Path): FileMetadata?
override fun list(dir: Path): List<Path>
override fun source(file: Path): Source
// Note: ZIP FileSystem is read-only
// write operations throw UnsupportedOperationException
}val fs = FileSystem.SYSTEM
val zipPath = "/tmp/example.zip".toPath()
// Create a ZIP file (using standard Java ZIP APIs for writing)
java.util.zip.ZipOutputStream(fs.sink(zipPath).buffer().outputStream()).use { zipOut ->
// Add first file
zipOut.putNextEntry(java.util.zip.ZipEntry("hello.txt"))
zipOut.write("Hello, ZIP world!".toByteArray())
zipOut.closeEntry()
// Add second file in subdirectory
zipOut.putNextEntry(java.util.zip.ZipEntry("subdir/file2.txt"))
zipOut.write("File in subdirectory".toByteArray())
zipOut.closeEntry()
// Add empty directory
zipOut.putNextEntry(java.util.zip.ZipEntry("empty-dir/"))
zipOut.closeEntry()
}
// Read ZIP file using Okio ZipFileSystem
val zipFs = ZipFileSystem(zipPath)
// List contents of ZIP file
println("ZIP file contents:")
zipFs.listRecursively("/".toPath()).forEach { path ->
val metadata = zipFs.metadataOrNull(path)
val type = when {
metadata?.isDirectory == true -> "[DIR]"
metadata?.isRegularFile == true -> "[FILE]"
else -> "[OTHER]"
}
val size = metadata?.size?.let { " (${it} bytes)" } ?: ""
println("$type $path$size")
}
// Read files from ZIP
val helloContent = zipFs.read("/hello.txt".toPath()) {
readUtf8()
}
println("Content of hello.txt: '$helloContent'")
val subdirContent = zipFs.read("/subdir/file2.txt".toPath()) {
readUtf8()
}
println("Content of subdir/file2.txt: '$subdirContent'")
// Check ZIP comment
println("ZIP comment: ${zipFs.comment}")
// Cleanup
fs.delete(zipPath)val zipFs = ZipFileSystem(zipPath)
// Get detailed metadata for ZIP entries
zipFs.list("/".toPath()).forEach { path ->
val metadata = zipFs.metadata(path)
println("Path: $path")
println(" Type: ${if (metadata.isDirectory) "Directory" else "File"}")
println(" Size: ${metadata.size} bytes")
metadata.lastModifiedAtMillis?.let { timestamp ->
val date = java.util.Date(timestamp)
println(" Modified: $date")
}
// ZIP-specific metadata in extras
metadata.extras.forEach { (type, value) ->
println(" Extra ${type.simpleName}: $value")
}
}// Utility function to compare compression methods
fun compareCompressionMethods(data: String) {
val originalSize = data.length
// GZIP compression
val gzipBuffer = Buffer()
gzipBuffer.gzip().use { sink ->
sink.writeUtf8(data)
}
val gzipSize = gzipBuffer.size
// Raw deflate compression
val deflateBuffer = Buffer()
val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, nowrap = true)
DeflaterSink(deflateBuffer, deflater).use { sink ->
sink.writeUtf8(data)
}
val deflateSize = deflateBuffer.size
println("Original: $originalSize bytes")
println("GZIP: $gzipSize bytes (${gzipSize.toFloat() / originalSize * 100}%)")
println("Deflate: $deflateSize bytes (${deflateSize.toFloat() / originalSize * 100}%)")
println("GZIP overhead: ${gzipSize - deflateSize} bytes")
}
// Test with different types of data
compareCompressionMethods("A".repeat(1000)) // Highly repetitive
compareCompressionMethods("The quick brown fox jumps over the lazy dog. ".repeat(50)) // Natural text
compareCompressionMethods((0..255).map { it.toChar() }.joinToString("")) // Random-like data// Compress large amounts of data without loading everything into memory
fun compressLargeFile(inputPath: Path, outputPath: Path) {
val fs = FileSystem.SYSTEM
fs.sink(outputPath).buffer().gzip().use { compressedSink ->
fs.source(inputPath).buffer().use { source ->
// Process in chunks to avoid memory issues
while (!source.exhausted()) {
val chunk = source.readByteString(minOf(8192L, source.buffer.size))
compressedSink.write(chunk)
}
}
}
}
// Decompress with progress monitoring
fun decompressWithProgress(inputPath: Path, outputPath: Path) {
val fs = FileSystem.SYSTEM
val totalSize = fs.metadata(inputPath).size ?: 0L
var processedBytes = 0L
fs.sink(outputPath).buffer().use { output ->
fs.source(inputPath).buffer().gzip().use { compressedSource ->
val buffer = Buffer()
while (!compressedSource.exhausted()) {
val bytesRead = compressedSource.read(buffer, 8192L)
if (bytesRead > 0) {
output.write(buffer, bytesRead)
processedBytes += bytesRead
val progress = (processedBytes.toFloat() / totalSize * 100).toInt()
print("\rDecompressing: $progress%")
}
}
}
}
println("\nDecompression complete!")
}Compression operations can encounter various error conditions:
expect open class IOException : Exception
expect class DataFormatException : Exception // Invalid compressed data format
expect class ZipException : IOException // ZIP file format issues// Handle corrupted compressed data
fun safeDecompress(compressedData: ByteString): String? {
return try {
Buffer().write(compressedData).gzip().buffer().readUtf8()
} catch (e: DataFormatException) {
println("Corrupted compressed data: ${e.message}")
null
} catch (e: IOException) {
println("I/O error during decompression: ${e.message}")
null
}
}
// Handle ZIP file errors
fun safeReadZip(zipPath: Path): List<String> {
return try {
val zipFs = ZipFileSystem(zipPath)
zipFs.listRecursively("/".toPath())
.filter { path -> zipFs.metadata(path).isRegularFile }
.map { path -> path.toString() }
.toList()
} catch (e: ZipException) {
println("Invalid ZIP file: ${e.message}")
emptyList()
} catch (e: FileNotFoundException) {
println("ZIP file not found: ${e.message}")
emptyList()
}
}
// Resource cleanup with error handling
fun compressWithCleanup(data: String): ByteString? {
val deflater = Deflater()
val buffer = Buffer()
return try {
DeflaterSink(buffer, deflater).use { sink ->
sink.writeUtf8(data)
}
buffer.readByteString()
} catch (e: Exception) {
println("Compression failed: ${e.message}")
null
} finally {
// Ensure native resources are released
deflater.end()
}
}// Efficient streaming for large data
fun efficientCompression(inputSource: Source, outputSink: Sink) {
// Use buffered streams to optimize I/O
val bufferedOutput = outputSink.buffer()
val gzipSink = bufferedOutput.gzip()
inputSource.buffer().use { bufferedInput ->
gzipSink.use { compressor ->
// Process in reasonable chunks
val buffer = Buffer()
while (!bufferedInput.exhausted()) {
val bytesRead = bufferedInput.read(buffer, 16384L) // 16KB chunks
if (bytesRead > 0) {
compressor.write(buffer, bytesRead)
}
}
}
}
}// Benchmark different compression levels
fun benchmarkCompression(data: ByteString) {
val levels = listOf(
Deflater.BEST_SPEED to "Best Speed",
Deflater.DEFAULT_COMPRESSION to "Default",
Deflater.BEST_COMPRESSION to "Best Compression"
)
levels.forEach { (level, name) ->
val startTime = System.currentTimeMillis()
val buffer = Buffer()
val deflater = Deflater(level, nowrap = false)
DeflaterSink(buffer, deflater).use { sink ->
sink.write(data)
}
val endTime = System.currentTimeMillis()
val compressedSize = buffer.size
val ratio = compressedSize.toFloat() / data.size
println("$name: ${compressedSize} bytes (${ratio * 100}%) in ${endTime - startTime}ms")
deflater.end()
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-squareup-okio--okio