diff --git a/uhabits-core/build.gradle.kts b/uhabits-core/build.gradle.kts index 2d558e93..1fe48e04 100644 --- a/uhabits-core/build.gradle.kts +++ b/uhabits-core/build.gradle.kts @@ -81,6 +81,7 @@ kotlin { dependencies { implementation(npm("sql.js", "1.11.0")) implementation(npm("sprintf-js", "1.1.3")) + implementation(npm("jszip", "3.10.1")) } } diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/RunSuspend.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/RunSuspend.kt deleted file mode 100644 index 416ef821..00000000 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/RunSuspend.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2016-2025 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ -package org.isoron.platform - -expect fun runSuspend(block: suspend () -> T): T diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/ZipWriter.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/Zip.kt similarity index 86% rename from uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/ZipWriter.kt rename to uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/Zip.kt index 77eb9aae..5135c106 100644 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/ZipWriter.kt +++ b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/Zip.kt @@ -19,6 +19,12 @@ package org.isoron.platform.io +class ZipEntry(val name: String, val content: String) + +expect class ZipReader(bytes: ByteArray) { + suspend fun entries(): List +} + expect class ZipWriter() { fun addEntry(name: String, content: String) suspend fun toBytes(): ByteArray diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/ZipReader.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/ZipReader.kt deleted file mode 100644 index 68eac000..00000000 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/io/ZipReader.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2016-2025 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.platform.io - -class ZipEntry(val name: String, val content: String) - -expect class ZipReader(bytes: ByteArray) { - fun entries(): List -} diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/CoroutineTaskRunner.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/CoroutineTaskRunner.kt index 3dbe442e..f1b18120 100644 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/CoroutineTaskRunner.kt +++ b/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/CoroutineTaskRunner.kt @@ -20,7 +20,9 @@ package org.isoron.uhabits.core.tasks import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -31,6 +33,7 @@ class CoroutineTaskRunner( private val scope = CoroutineScope(SupervisorJob() + mainDispatcher) private val listeners = mutableListOf() + private val jobs = mutableListOf() private var activeCount = 0 override val activeTaskCount: Int get() = activeCount @@ -45,7 +48,7 @@ class CoroutineTaskRunner( override fun execute(task: Task) { task.onAttached(this) - scope.launch { + val job = scope.launch { activeCount++ listeners.forEach { it.onTaskStarted(task) } task.onPreExecute() @@ -58,6 +61,13 @@ class CoroutineTaskRunner( activeCount-- listeners.forEach { it.onTaskFinished(task) } } + job.invokeOnCompletion { jobs.remove(job) } + jobs.add(job) + } + + override suspend fun await() { + jobs.joinAll() + jobs.clear() } override fun publishProgress(task: Task, progress: Int) { diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/TaskRunner.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/TaskRunner.kt index 795acde1..bd291988 100644 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/TaskRunner.kt +++ b/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/tasks/TaskRunner.kt @@ -29,4 +29,6 @@ interface TaskRunner { fun onTaskStarted(task: Task) fun onTaskFinished(task: Task) } + + suspend fun await() } diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 47801c23..4e072b83 100644 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/commonMain/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -89,9 +89,7 @@ open class ListHabitsBehavior( if (filename != null) { screen.showSendFileScreen(filename) } else { - screen.showMessage( - Message.COULD_NOT_EXPORT - ) + screen.showMessage(Message.COULD_NOT_EXPORT) } } ) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/platform/io/ZipWriterTest.kt b/uhabits-core/src/commonTest/kotlin/org/isoron/platform/io/ZipTest.kt similarity index 75% rename from uhabits-core/src/jvmTest/java/org/isoron/platform/io/ZipWriterTest.kt rename to uhabits-core/src/commonTest/kotlin/org/isoron/platform/io/ZipTest.kt index 0a4f4e9b..fc120a74 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/platform/io/ZipWriterTest.kt +++ b/uhabits-core/src/commonTest/kotlin/org/isoron/platform/io/ZipTest.kt @@ -19,17 +19,15 @@ package org.isoron.platform.io -import kotlinx.coroutines.runBlocking -import java.io.ByteArrayInputStream -import java.util.zip.ZipInputStream +import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class ZipWriterTest { +class ZipTest { @Test - fun testSingleEntry() = runBlocking { + fun testSingleEntry() = runTest { val zip = ZipWriter() zip.addEntry("hello.txt", "Hello, World!") val bytes = zip.toBytes() @@ -40,7 +38,7 @@ class ZipWriterTest { } @Test - fun testMultipleEntries() = runBlocking { + fun testMultipleEntries() = runTest { val zip = ZipWriter() zip.addEntry("a.csv", "name,value\nfoo,1\n") zip.addEntry("subdir/b.csv", "col1,col2\nbar,2\n") @@ -55,7 +53,7 @@ class ZipWriterTest { } @Test - fun testEmptyContent() = runBlocking { + fun testEmptyContent() = runTest { val zip = ZipWriter() zip.addEntry("empty.txt", "") val bytes = zip.toBytes() @@ -66,7 +64,7 @@ class ZipWriterTest { } @Test - fun testLargeContent() = runBlocking { + fun testLargeContent() = runTest { val zip = ZipWriter() val large = "x".repeat(100_000) zip.addEntry("large.txt", large) @@ -77,16 +75,7 @@ class ZipWriterTest { assertEquals(large, entries["large.txt"]) } - private fun readZipEntries(bytes: ByteArray): Map { - val result = mutableMapOf() - val zis = ZipInputStream(ByteArrayInputStream(bytes)) - var entry = zis.nextEntry - while (entry != null) { - result[entry.name] = zis.readBytes().decodeToString() - zis.closeEntry() - entry = zis.nextEntry - } - zis.close() - return result + private suspend fun readZipEntries(bytes: ByteArray): Map { + return ZipReader(bytes).entries().associate { it.name to it.content } } } diff --git a/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt b/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt index f51803d3..fa7b2c55 100644 --- a/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt +++ b/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt @@ -95,17 +95,19 @@ class ListHabitsBehaviorTest : BaseUnitTest() { val outputDir = createTempDir() every { dirFinder.getCSVOutputDir() } returns outputDir behavior.onExportCSV() + taskRunner.await() verify { screen.showSendFileScreen(any()) } val files = outputDir.listFiles() assertEquals(1, files!!.size) } @Test - fun testOnExportCSV_fail() { + fun testOnExportCSV_fail() = runTest { val mockDir: UserFile = mock() every { mockDir.resolve(any()) } throws RuntimeException("not writable") every { dirFinder.getCSVOutputDir() } returns mockDir behavior.onExportCSV() + taskRunner.await() verify { screen.showMessage(ListHabitsBehavior.Message.COULD_NOT_EXPORT) } } diff --git a/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenterTest.kt b/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenterTest.kt index 5428001e..ff32ae0f 100644 --- a/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenterTest.kt +++ b/uhabits-core/src/commonTest/kotlin/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenterTest.kt @@ -25,6 +25,7 @@ import dev.mokkery.verify import kotlinx.coroutines.test.runTest import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.tasks.CoroutineTaskRunner import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -62,6 +63,7 @@ class ShowHabitMenuPresenterTest : BaseUnitTest() { val outputDir = createTempDir() every { system.getCSVOutputDir() } returns outputDir menu.onExportCSV() + (taskRunner as CoroutineTaskRunner).await() val files = outputDir.listFiles() assertEquals(1, files!!.size) } diff --git a/uhabits-core/src/jsMain/kotlin/org/isoron/platform/RunSuspend.kt b/uhabits-core/src/jsMain/kotlin/org/isoron/platform/RunSuspend.kt deleted file mode 100644 index abcfefbc..00000000 --- a/uhabits-core/src/jsMain/kotlin/org/isoron/platform/RunSuspend.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.isoron.platform - -import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine - -actual fun runSuspend(block: suspend () -> T): T { - var result: Result? = null - block.startCoroutine( - object : Continuation { - override val context = EmptyCoroutineContext - override fun resumeWith(r: Result) { - result = r - } - } - ) - return result?.getOrThrow() - ?: throw IllegalStateException( - "runSuspend: coroutine did not complete synchronously" - ) -} diff --git a/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/Zip.kt b/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/Zip.kt new file mode 100644 index 00000000..bfd9db52 --- /dev/null +++ b/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/Zip.kt @@ -0,0 +1,53 @@ +package org.isoron.platform.io + +import kotlinx.coroutines.await +import org.khronos.webgl.Uint8Array +import org.khronos.webgl.get +import org.khronos.webgl.set +import kotlin.js.Promise +import kotlin.js.json + +@JsModule("jszip") +@JsNonModule +external class JSZip { + fun loadAsync(data: dynamic): Promise + fun file(name: String, data: String): JSZip + fun forEach(callback: (String, dynamic) -> Unit) + fun generateAsync(options: dynamic): Promise +} + +actual class ZipReader actual constructor(bytes: ByteArray) { + private val data = bytes + + actual suspend fun entries(): List { + val uint8 = Uint8Array(data.size) + for (i in data.indices) uint8[i] = data[i] + val zip = JSZip().loadAsync(uint8).await() + val result = mutableListOf() + val files = mutableListOf>() + zip.forEach { path, file -> + if (!(file.dir as Boolean)) { + files.add(path to file) + } + } + for ((name, file) in files) { + val content = (file.async("string") as Promise).await() + result.add(ZipEntry(name, content)) + } + return result + } +} + +actual class ZipWriter { + private val zip = JSZip() + + actual fun addEntry(name: String, content: String) { + zip.file(name, content) + } + + actual suspend fun toBytes(): ByteArray { + val uint8 = + (zip.generateAsync(json("type" to "uint8array", "compression" to "DEFLATE")) as Promise).await() + return ByteArray(uint8.length) { uint8[it] } + } +} diff --git a/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/ZipReader.kt b/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/ZipReader.kt deleted file mode 100644 index 738ccb6d..00000000 --- a/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/ZipReader.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.isoron.platform.io - -actual class ZipReader actual constructor(bytes: ByteArray) { - private val data = bytes - - actual fun entries(): List { - val result = mutableListOf() - var pos = 0 - while (pos + 30 <= data.size) { - val sig = readInt(pos) - if (sig != 0x04034b50) break - val compressionMethod = readShort(pos + 8) - val compressedSize = readInt(pos + 18) - val nameLen = readShort(pos + 26) - val extraLen = readShort(pos + 28) - val nameStart = pos + 30 - val name = data.copyOfRange(nameStart, nameStart + nameLen).decodeToString() - val dataStart = nameStart + nameLen + extraLen - if (compressionMethod != 0) { - throw UnsupportedOperationException( - "Only STORED ZIP entries are supported, got method $compressionMethod" - ) - } - val content = data.copyOfRange(dataStart, dataStart + compressedSize).decodeToString() - result.add(ZipEntry(name, content)) - pos = dataStart + compressedSize - } - return result - } - - private fun readInt(offset: Int): Int = - (data[offset].toInt() and 0xFF) or - ((data[offset + 1].toInt() and 0xFF) shl 8) or - ((data[offset + 2].toInt() and 0xFF) shl 16) or - ((data[offset + 3].toInt() and 0xFF) shl 24) - - private fun readShort(offset: Int): Int = - (data[offset].toInt() and 0xFF) or - ((data[offset + 1].toInt() and 0xFF) shl 8) -} diff --git a/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/ZipWriter.kt b/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/ZipWriter.kt deleted file mode 100644 index a438a79a..00000000 --- a/uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/ZipWriter.kt +++ /dev/null @@ -1,122 +0,0 @@ -package org.isoron.platform.io - -actual class ZipWriter { - private val entries = mutableListOf>() - - actual fun addEntry(name: String, content: String) { - entries.add(name to content.encodeToByteArray()) - } - - actual suspend fun toBytes(): ByteArray { - val buf = ZipBuffer() - val offsets = mutableListOf() - - for ((name, data) in entries) { - offsets.add(buf.size) - val nameBytes = name.encodeToByteArray() - val crc = crc32(data) - buf.writeInt(0x04034b50) - buf.writeShort(20) - buf.writeShort(0) - buf.writeShort(0) - buf.writeShort(0) - buf.writeShort(0) - buf.writeInt(crc) - buf.writeInt(data.size) - buf.writeInt(data.size) - buf.writeShort(nameBytes.size) - buf.writeShort(0) - buf.writeBytes(nameBytes) - buf.writeBytes(data) - } - - val centralDirOffset = buf.size - for (i in entries.indices) { - val (name, data) = entries[i] - val nameBytes = name.encodeToByteArray() - val crc = crc32(data) - buf.writeInt(0x02014b50) - buf.writeShort(20) - buf.writeShort(20) - buf.writeShort(0) - buf.writeShort(0) - buf.writeShort(0) - buf.writeShort(0) - buf.writeInt(crc) - buf.writeInt(data.size) - buf.writeInt(data.size) - buf.writeShort(nameBytes.size) - buf.writeShort(0) - buf.writeShort(0) - buf.writeShort(0) - buf.writeShort(0) - buf.writeInt(0) - buf.writeInt(offsets[i]) - buf.writeBytes(nameBytes) - } - val centralDirSize = buf.size - centralDirOffset - - buf.writeInt(0x06054b50) - buf.writeShort(0) - buf.writeShort(0) - buf.writeShort(entries.size) - buf.writeShort(entries.size) - buf.writeInt(centralDirSize) - buf.writeInt(centralDirOffset) - buf.writeShort(0) - - return buf.toByteArray() - } -} - -private class ZipBuffer { - private var data = ByteArray(4096) - var size = 0 - private set - - fun writeShort(v: Int) { - ensureCapacity(2) - data[size++] = (v and 0xFF).toByte() - data[size++] = ((v shr 8) and 0xFF).toByte() - } - - fun writeInt(v: Int) { - ensureCapacity(4) - data[size++] = (v and 0xFF).toByte() - data[size++] = ((v shr 8) and 0xFF).toByte() - data[size++] = ((v shr 16) and 0xFF).toByte() - data[size++] = ((v shr 24) and 0xFF).toByte() - } - - fun writeBytes(bytes: ByteArray) { - ensureCapacity(bytes.size) - bytes.copyInto(data, size) - size += bytes.size - } - - fun toByteArray(): ByteArray = data.copyOf(size) - - private fun ensureCapacity(needed: Int) { - if (size + needed > data.size) { - data = data.copyOf(maxOf(data.size * 2, size + needed)) - } - } -} - -private val crcTable = IntArray(256).also { table -> - for (n in 0..255) { - var c = n - repeat(8) { - c = if (c and 1 != 0) (c ushr 1) xor 0xEDB88320.toInt() else c ushr 1 - } - table[n] = c - } -} - -private fun crc32(data: ByteArray): Int { - var crc = -1 - for (b in data) { - crc = (crc ushr 8) xor crcTable[(crc xor b.toInt()) and 0xFF] - } - return crc xor -1 -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/platform/JvmRunSuspend.kt b/uhabits-core/src/jvmMain/java/org/isoron/platform/JvmRunSuspend.kt deleted file mode 100644 index a8ad2452..00000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/platform/JvmRunSuspend.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2016-2025 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ -package org.isoron.platform - -import kotlinx.coroutines.runBlocking - -actual fun runSuspend(block: suspend () -> T): T = runBlocking { block() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/platform/io/ZipReader.kt b/uhabits-core/src/jvmMain/java/org/isoron/platform/io/Zip.kt similarity index 71% rename from uhabits-core/src/jvmMain/java/org/isoron/platform/io/ZipReader.kt rename to uhabits-core/src/jvmMain/java/org/isoron/platform/io/Zip.kt index cb4ea7f1..c4c9672b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/platform/io/ZipReader.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/platform/io/Zip.kt @@ -20,12 +20,14 @@ package org.isoron.platform.io import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream actual class ZipReader actual constructor(bytes: ByteArray) { private val data = bytes - actual fun entries(): List { + actual suspend fun entries(): List { val result = mutableListOf() val zis = ZipInputStream(ByteArrayInputStream(data)) var entry = zis.nextEntry @@ -38,3 +40,19 @@ actual class ZipReader actual constructor(bytes: ByteArray) { return result } } + +actual class ZipWriter { + private val baos = ByteArrayOutputStream() + private val zos = ZipOutputStream(baos) + + actual fun addEntry(name: String, content: String) { + zos.putNextEntry(java.util.zip.ZipEntry(name)) + zos.write(content.toByteArray()) + zos.closeEntry() + } + + actual suspend fun toBytes(): ByteArray { + zos.close() + return baos.toByteArray() + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/platform/io/ZipWriter.kt b/uhabits-core/src/jvmMain/java/org/isoron/platform/io/ZipWriter.kt deleted file mode 100644 index 876083b9..00000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/platform/io/ZipWriter.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2016-2025 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.platform.io - -import java.io.ByteArrayOutputStream -import java.util.zip.ZipOutputStream - -actual class ZipWriter { - private val baos = ByteArrayOutputStream() - private val zos = ZipOutputStream(baos) - - actual fun addEntry(name: String, content: String) { - zos.putNextEntry(java.util.zip.ZipEntry(name)) - zos.write(content.toByteArray()) - zos.closeEntry() - } - - actual suspend fun toBytes(): ByteArray { - zos.close() - return baos.toByteArray() - } -}