package org.meshbay.client.save /** The pure part of saving: names, types and the binary frame. JVM-tested. */ object SaveNames { private val BIDI = Regex("[\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069]") /** * main.js `save:begin`: the basename only, bidirectional controls replaced * — "invoice‮fdp.exe" would otherwise be listed as "invoiceexe.pdf". */ fun sanitize(suggested: String?): String { val base = (suggested ?: "").replace('\\', '/').substringAfterLast('/') .replace(BIDI, "_").replace(Regex("[\\u0000-\\u001f]"), "_").trim() return if (base.isEmpty() || base == "." || base == "..") "download" else base } /** * downloads.js `OPENABLE`, entry for entry (test_android_downloads.py): * what may be handed to another app to open, under a type chosen from the * name — never one guessed from the bytes. */ val OPENABLE = mapOf( "pdf" to "application/pdf", "png" to "image/png", "jpg" to "image/jpeg", "jpeg" to "image/jpeg", "gif" to "image/gif", "webp" to "image/webp", "avif" to "image/avif", "bmp" to "image/bmp", "mp3" to "audio/mpeg", "m4a" to "audio/mp4", "aac" to "audio/aac", "ogg" to "audio/ogg", "oga" to "audio/ogg", "opus" to "audio/ogg", "flac" to "audio/flac", "wav" to "audio/wav", "mp4" to "video/mp4", "m4v" to "video/mp4", "webm" to "video/webm", "ogv" to "video/ogg", "mov" to "video/quicktime", "txt" to "text/plain", "log" to "text/plain", "md" to "text/plain", "csv" to "text/plain", ) fun extension(name: String): String? = Regex("\\.([A-Za-z0-9]+)$").find(name)?.groupValues?.get(1)?.lowercase() fun openableType(name: String): String? = extension(name)?.let { OPENABLE[it] } /** The type a file is created under: openable ones by name, the rest opaque. */ fun storedType(name: String): String = openableType(name) ?: "application/octet-stream" /** "name (n).ext", as main.js `freeName` — never an overwrite. */ fun numbered(name: String, n: Int): String { val ext = extension(name)?.let { ".$it" } ?: "" val stem = if (ext.isEmpty()) name else name.dropLast(ext.length) return "$stem ($n)$ext" } } /** * A binary bridge message: one write, no JSON, no base64. * * "MBB1" | u32 request id | u16 channel | u16 reserved | u32 handle | bytes * * all big-endian. The reply is an ordinary JSON reply carrying the id. */ class BinaryFrame(val id: Long, val channel: Int, val handle: Long, val bytes: ByteArray, val offset: Int) { val length get() = bytes.size - offset companion object { const val HEADER = 16 const val SAVE_WRITE = 1 const val CAST_PUSH = 2 const val CAST_FILE = 3 private val MAGIC = byteArrayOf('M'.code.toByte(), 'B'.code.toByte(), 'B'.code.toByte(), '1'.code.toByte()) private fun u32(b: ByteArray, at: Int) = ((b[at].toLong() and 0xff) shl 24) or ((b[at + 1].toLong() and 0xff) shl 16) or ((b[at + 2].toLong() and 0xff) shl 8) or (b[at + 3].toLong() and 0xff) fun parse(b: ByteArray): BinaryFrame? { if (b.size < HEADER || !(0 until 4).all { b[it] == MAGIC[it] }) return null val channel = ((b[8].toInt() and 0xff) shl 8) or (b[9].toInt() and 0xff) return BinaryFrame(u32(b, 4), channel, u32(b, 12), b, HEADER) } } }