diff --git a/common/src/linuxX64Main/kotlin/ActualMPPFile.kt b/common/src/linuxX64Main/kotlin/ActualMPPFile.kt new file mode 100644 index 00000000000..4c00ce4fbdc --- /dev/null +++ b/common/src/linuxX64Main/kotlin/ActualMPPFile.kt @@ -0,0 +1,146 @@ +package dev.inmo.micro_utils.common + +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.readBytes +import platform.linux.statvfs +import platform.posix.ACCESSPERMS +import platform.posix.FILE +import platform.posix.F_OK +import platform.posix.SEEK_END +import platform.posix.S_IFDIR +import platform.posix.S_IFMT +import platform.posix.access +import platform.posix.fgets +import platform.posix.fopen +import platform.posix.fseek +import platform.posix.ftell +import platform.posix.remove +import platform.posix.rename +import platform.posix.rmdir +import platform.posix.stat + +actual data class MPPFile(internal val filename: FileName) { + val path = filename.string + val isFile: Boolean + get() = memScoped { + val stat = alloc() + if (stat(path, stat.ptr) != 0) + return@memScoped false + (S_IFDIR != (stat.st_mode and S_IFMT.convert()).convert()) + } + + val isDirectory: Boolean + get() = memScoped { + val stat = alloc() + if (stat(path, stat.ptr) != 0) + return@memScoped false + S_IFDIR == (stat.st_mode and S_IFMT.convert()).convert() + } + val size: Long + get() = memScoped { + val stat = alloc() + if (stat(path, stat.ptr) != 0) + return@memScoped 0 + return stat.st_size.convert() + } + val lastModified: Long + get() = memScoped { + val stat = alloc() + if (stat(path, stat.ptr) != 0) + return@memScoped 0 + return stat.st_ctim.tv_nsec / 1000L + } + + val freeSpace: Long + get() = + memScoped { + val stat = alloc() + statvfs(path, stat.ptr) + (stat.f_bfree.toULong() * stat.f_bsize.toULong()).toLong() + } + + val availableSpace: Long + get() = + memScoped { + val stat = alloc() + statvfs(path, stat.ptr) + (stat.f_bavail.toULong() * stat.f_bsize.toULong()).toLong() + } + + val totalSpace: Long + get() = memScoped { + val stat = alloc() + statvfs(path, stat.ptr) + (stat.f_blocks.toULong() * stat.f_frsize.toULong()).toLong() + } + + constructor(vararg path: String) : this(FileName(path.map { it.removeSuffix(SEPARATOR_STRING) }.joinToString(SEPARATOR_STRING))) + constructor(parent: MPPFile, subpath: String) : this("${parent.filename.withoutSlashAtTheEnd}$SEPARATOR${subpath}") + + fun createPointer(mode: String = "r") = fopen(filename.name, mode) + + fun delete(): Boolean { + return when { + isDirectory -> rmdir(path) == 0 + isFile -> remove(path) == 0 + else -> false + } + } + + fun mkdir(): Boolean = platform.posix.mkdir(path, ACCESSPERMS) == 0 + + override fun toString(): String = path + override fun equals(other: Any?): Boolean { + if (other !is MPPFile) return false + return path == other.path + } + + override fun hashCode(): Int = 31 + path.hashCode() + + fun renameTo(newPath: MPPFile): Boolean = rename(path, newPath.path) == 0 + + fun list(): List { + val out = ArrayList() + FileIterator(this).forEach { file -> + out += file + } + return out + } + + fun fileChannel(mode: String = "r") = FileChannel(this, mode) + + companion object { + val SEPARATOR: Char = '/' + val SEPARATOR_STRING: String = SEPARATOR.toString() + val temporalDirectory: MPPFile? + get() = MPPFile("/tmp").takeIf { + it.isDirectory + } + } +} + +actual val MPPFile.filename: FileName + get() = this.filename +actual val MPPFile.filesize: Long + get() = memScoped { + val pointer = createPointer() + fseek(pointer, 0L, SEEK_END) + ftell(pointer) + } +actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator + get() = { + memScoped { + fileChannel().readFully() + } + } +actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator + get() = { + bytesAllocatorSync() + } + diff --git a/common/src/linuxX64Main/kotlin/FileChannel.kt b/common/src/linuxX64Main/kotlin/FileChannel.kt new file mode 100644 index 00000000000..d76ba8f66cd --- /dev/null +++ b/common/src/linuxX64Main/kotlin/FileChannel.kt @@ -0,0 +1,107 @@ +package dev.inmo.micro_utils.common + +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toCValues +import kotlinx.cinterop.toKString +import platform.posix.SEEK_END +import platform.posix.SEEK_SET +import platform.posix.fclose +import platform.posix.feof +import platform.posix.fread +import platform.posix.fseek +import platform.posix.ftell +import platform.posix.fwrite +import platform.posix.size_t + +class FileChannel (file: MPPFile, mode: String) : RandomAccess { + internal val handler = file.createPointer(mode) + + private var position: ULong + get() = ftell(handler).convert() + set(value) { + fseek(handler, value.convert(), SEEK_SET) + } + private val size: ULong + get() { + val pos = position + gotoEnd() + val result = position + position = pos + return result + } + + fun skip(length: Long): Long { + checkClosed() + memScoped { } + if (length == 0L) + return 0L + if (feof(handler) != 0) + return 0L + val endOfFile = size + val position = minOf(endOfFile, this.position + length.toULong()) + this.position = position + return (endOfFile - position).toLong() + } + + fun read(dest: ByteArray): Int { + checkClosed() + if (feof(handler) != 0) + return 0 + + memScoped { + val tmp = allocArray(dest.size); + fread(tmp, Byte.SIZE_BYTES.convert(), dest.size.convert(), handler).convert() + tmp.toKString() + } + return fread(dest.toCValues(), Byte.SIZE_BYTES.convert(), dest.size.convert(), handler).convert() + } + + private var closed = false + + private fun checkClosed() { + if (closed) { + error("File channel has been closed already") + } + } + + fun close() { + checkClosed() + fclose(handler) + closed = true + } + + fun write(data: ByteArray): Int { + checkClosed() + if (feof(handler) != 0) + return 0 + + return fwrite(data.toCValues(), 1.convert(), data.size.convert(), handler).convert() + } + + fun flush() { + checkClosed() + } + + private fun gotoEnd() { + fseek(handler, 0, SEEK_END) + } + + fun readFully(): ByteArray { + var result = ByteArray(0) + memScoped { + val tmp = ByteArray(64 * 1024) + + do { + val read = read(tmp) + result += tmp.take(read) + } while (read > 0) + } + val tmp = ByteArray(64 * 1024) + + + return result + } +} diff --git a/common/src/linuxX64Main/kotlin/FileIterator.kt b/common/src/linuxX64Main/kotlin/FileIterator.kt new file mode 100644 index 00000000000..95d7490f130 --- /dev/null +++ b/common/src/linuxX64Main/kotlin/FileIterator.kt @@ -0,0 +1,56 @@ +package dev.inmo.micro_utils.common + +import kotlinx.cinterop.pointed +import kotlinx.cinterop.toKString +import platform.posix.closedir +import platform.posix.dirent +import platform.posix.opendir +import platform.posix.readdir +import kotlin.native.internal.createCleaner + +class FileIterator internal constructor(private val file: MPPFile) : Iterator { + init { + if (!file.isDirectory) + error("\"${file.path}\" is not direction") + } + + private val handler = opendir(file.path) + private var next: dirent? = null + private var end = false + + override fun hasNext(): Boolean { + while (true) { + if (end) + return false + + if (next == null) { + next = readdir(handler)?.pointed + if (next == null) { + end = true + return false + } + val name = next!!.d_name.toKString() + if (name == "." || name == "..") { + next = null + continue + } + return true + } + return true + } + } + + override fun next(): MPPFile { + if (!hasNext()) + throw NoSuchElementException() + val result = MPPFile(file, next!!.d_name.toKString()) + next = null + return result + } + + @OptIn(ExperimentalStdlibApi::class) + private val cleaner = createCleaner(handler) { + closedir(it) + } +} + diff --git a/common/src/linuxX64Main/kotlin/fixed.kt b/common/src/linuxX64Main/kotlin/fixed.kt new file mode 100644 index 00000000000..057002f1ad4 --- /dev/null +++ b/common/src/linuxX64Main/kotlin/fixed.kt @@ -0,0 +1,26 @@ +package dev.inmo.micro_utils.common + +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKString +import platform.posix.snprintf +import platform.posix.sprintf + +actual fun Float.fixed(signs: Int): Float { + return memScoped { + val buff = allocArray(Float.SIZE_BYTES * 2) + + sprintf(buff, "%.${signs}f", this@fixed) + buff.toKString().toFloat() + } +} + +actual fun Double.fixed(signs: Int): Double { + return memScoped { + val buff = allocArray(Double.SIZE_BYTES * 2) + + sprintf(buff, "%.${signs}f", this@fixed) + buff.toKString().toDouble() + } +} diff --git a/common/src/linuxX64Test/kotlin/FileTests.kt b/common/src/linuxX64Test/kotlin/FileTests.kt new file mode 100644 index 00000000000..5be272eb8b3 --- /dev/null +++ b/common/src/linuxX64Test/kotlin/FileTests.kt @@ -0,0 +1,36 @@ +package dev.inmo.micro_utils.common + +import dev.inmo.micro_utils.common.MPPFile +import kotlin.test.Test +import kotlin.test.assertEquals + +fun createTestFile(content: String): MPPFile = MPPFile( + MPPFile.temporalDirectory!!, + "tmp.file.txt" +).apply { + val channel = fileChannel("wc") + runCatching { + channel.write(content.encodeToByteArray()) + } + channel.close() +} +fun MPPFile.removeTestFile() { + delete() +} + +class FileTests { + @Test + fun testReadFromFile() { + val testContent = "Test" + val file = createTestFile(testContent) + + try { + val content = runCatching { + file.fileChannel().readFully().contentToString() + }.getOrThrow() + assertEquals(testContent, content) + } finally { + file.removeTestFile() + } + } +} diff --git a/mppProjectWithSerialization.gradle b/mppProjectWithSerialization.gradle index 3e6c678a097..3ee345baf44 100644 --- a/mppProjectWithSerialization.gradle +++ b/mppProjectWithSerialization.gradle @@ -18,7 +18,6 @@ kotlin { android { publishAllLibraryVariants() } - linuxArm64() linuxX64() mingwX64() @@ -53,12 +52,6 @@ kotlin { implementation libs.android.espresso } } - linuxArm64Test { - dependencies { - implementation kotlin('test-js') - implementation kotlin('test-junit') - } - } mingwX64Test { dependencies { implementation kotlin('test-js')