aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/save/SaveNames.kt
blob: 40250c7ed26ca3ab1b760e8f9a2c10748c818d02 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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)
        }
    }
}