From 5cd05ab0084d7a1338d468869b5efdb096965e57 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 01:18:47 +0000 Subject: [PATCH] feat(quartz/ios): implement GZip compress/decompress for iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses platform.zlib (built-in on all Apple targets) via Kotlin/Native cinterop to provide the actual GZip implementation: - compress: deflateInit2 with windowBits=31 (MAX_WBITS+16) for gzip format; deflateBound gives a tight upper-bound so a single Z_FINISH call is always enough – no output loop required. - decompress: inflateInit2 with windowBits=47 (MAX_WBITS+32) for automatic gzip/zlib format detection; output is collected in fixed-size chunks to handle arbitrary decompressed sizes. https://claude.ai/code/session_0125CGfu6aMAnSv6ZzAxFHvV --- .../vitorpamplona/quartz/utils/GZip.ios.kt | 110 +++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt index 6db97fc26..cd7419a8c 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt @@ -20,12 +20,118 @@ */ package com.vitorpamplona.quartz.utils +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import platform.zlib.Z_DEFAULT_COMPRESSION +import platform.zlib.Z_DEFAULT_STRATEGY +import platform.zlib.Z_DEFLATED +import platform.zlib.Z_FINISH +import platform.zlib.Z_NO_FLUSH +import platform.zlib.Z_OK +import platform.zlib.Z_STREAM_END +import platform.zlib.deflate +import platform.zlib.deflateBound +import platform.zlib.deflateEnd +import platform.zlib.deflateInit2 +import platform.zlib.inflate +import platform.zlib.inflateEnd +import platform.zlib.inflateInit2 +import platform.zlib.z_stream + +@OptIn(ExperimentalForeignApi::class) actual object GZip { + /** + * Compresses [content] into a gzip byte array using zlib's deflateInit2 with + * windowBits=31 (MAX_WBITS + 16), which requests the gzip wrapper format. + * deflateBound gives an upper-bound on output size so a single deflate call + * with Z_FINISH is always sufficient. + */ actual fun compress(content: String): ByteArray { - TODO("Not yet implemented") + val input = content.encodeToByteArray() + + return memScoped { + val stream = alloc() + + // windowBits = 15 + 16 = 31 → gzip format + deflateInit2( + stream.ptr, + Z_DEFAULT_COMPRESSION, + Z_DEFLATED, + 31, + 8, + Z_DEFAULT_STRATEGY, + ).let { check(it == Z_OK) { "deflateInit2 failed: $it" } } + + val maxSize = deflateBound(stream.ptr, input.size.toULong()).toInt() + val output = ByteArray(maxSize) + + val written = + input.usePinned { pinIn -> + output.usePinned { pinOut -> + stream.next_in = pinIn.addressOf(0).reinterpret() + stream.avail_in = input.size.toUInt() + stream.next_out = pinOut.addressOf(0).reinterpret() + stream.avail_out = maxSize.toUInt() + + deflate(stream.ptr, Z_FINISH) + .let { check(it == Z_STREAM_END) { "deflate failed: $it" } } + + maxSize - stream.avail_out.toInt() + } + } + + deflateEnd(stream.ptr) + output.copyOf(written) + } } + /** + * Decompresses a gzip [content] byte array back to a UTF-8 string using + * zlib's inflateInit2 with windowBits=47 (MAX_WBITS + 32), which enables + * automatic detection of both gzip and zlib formats. + * Output is collected in fixed-size chunks to handle arbitrary output size. + */ actual fun decompress(content: ByteArray): String { - TODO("Not yet implemented") + val chunks = ArrayList() + val chunkSize = maxOf(content.size * 4, 4096) + + memScoped { + val stream = alloc() + + // windowBits = 15 + 32 = 47 → auto-detect gzip or zlib + inflateInit2(stream.ptr, 47) + .let { check(it == Z_OK) { "inflateInit2 failed: $it" } } + + content.usePinned { pinIn -> + stream.next_in = pinIn.addressOf(0).reinterpret() + stream.avail_in = content.size.toUInt() + + var status: Int + do { + val chunk = ByteArray(chunkSize) + chunk.usePinned { pinOut -> + stream.next_out = pinOut.addressOf(0).reinterpret() + stream.avail_out = chunkSize.toUInt() + status = inflate(stream.ptr, Z_NO_FLUSH) + val produced = chunkSize - stream.avail_out.toInt() + if (produced > 0) chunks.add(chunk.copyOf(produced)) + } + } while (status == Z_OK) + + check(status == Z_STREAM_END) { "inflate failed: $status" } + } + + inflateEnd(stream.ptr) + } + + val totalSize = chunks.sumOf { it.size } + val result = ByteArray(totalSize) + var pos = 0 + chunks.forEach { chunk -> chunk.copyInto(result, pos); pos += chunk.size } + return result.decodeToString() } }