Move SSD transport messages to typed Kotlin

ober

f1fc65296e3333de116bd40f30b94ad915c3c286

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index d340697..1958159 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1302,7 +1302,10 @@
                 storageSummaryText compressionRatioBudget boundedTreeReadLimit
                 boundedReadInitialCapacity mergedJsonLinesText
                 truthFileName reviewFileName eventsFileName
-                inputLimitExceededMessage outputLimitExceededMessage)
+                inputLimitExceededMessage outputLimitExceededMessage
+                clientLogHttpStatusMessage bearerAuthorizationHeader
+                zipEntryCountLimitExceededMessage httpStatusMessage
+                unableToCreateSyncFolderMessage unableToCreateFileMessage)
         (type Int32)
         (record ImportBudget
           ((mut entries : Int32)
@@ -1349,6 +1352,25 @@
           (string-append
             "Output exceeds "
             (string-append (int->string limit) " bytes")))
+        (def (clientLogHttpStatusMessage (code : Int32)) : String
+          (string-append
+            "client log HTTP "
+            (int32->string code)))
+        (def (bearerAuthorizationHeader (token : String)) : String
+          (string-append "Bearer " token))
+        (def (zipEntryCountLimitExceededMessage (entries : Int32)
+                                                (limit : Int32)) : String
+          (string-append
+            "ZIP entry-count limit exceeded: "
+            (string-append
+              (int32->string entries)
+              (string-append "/" (int32->string limit)))))
+        (def (httpStatusMessage (code : Int32)) : String
+          (string-append "HTTP " (int32->string code)))
+        (def (unableToCreateSyncFolderMessage (name : String)) : String
+          (string-append "Unable to create sync folder " name))
+        (def (unableToCreateFileMessage (fileName : String)) : String
+          (string-append "Unable to create " fileName))
         (def (mergedJsonLinesText (lines : (Set String))) : String
           (joinStringSet
             lines
@@ -6648,7 +6670,7 @@
        "        try {"
        "            connection.outputStream.use { it.write(bytes) }"
        "            val code = connection.responseCode"
-       "            if (code !in 200..299) throw IllegalStateException(\"client log HTTP $code\")"
+       "            if (code !in 200..299) throw IllegalStateException(clientLogHttpStatusMessage(code))"
        "            connection.inputStream.close()"
        "        } finally {"
        "            connection.disconnect()"
@@ -7775,7 +7797,7 @@
        "            connectTimeout = 5000"
        "            readTimeout = 30000"
        "            instanceFollowRedirects = false"
-       "            if (config.bearerToken.isNotBlank()) setRequestProperty(\"Authorization\", \"Bearer ${config.bearerToken}\")"
+       "            if (config.bearerToken.isNotBlank()) setRequestProperty(\"Authorization\", bearerAuthorizationHeader(config.bearerToken))"
        "            setRequestProperty(\"Accept\", \"application/json\")"
        "            if (acceptGzip) setRequestProperty(\"Accept-Encoding\", \"gzip\")"
        "            if (contentType != null) setRequestProperty(\"Content-Type\", contentType)"
@@ -7954,7 +7976,7 @@
        "                while (true) {"
        "                    val entry = zip.nextEntry ?: break"
        "                    budget.entries += 1"
-       "                    if (budget.entries > MAX_ZIP_ENTRIES) throw IllegalStateException(\"ZIP entry-count limit exceeded: ${budget.entries}/$MAX_ZIP_ENTRIES\")"
+       "                    if (budget.entries > MAX_ZIP_ENTRIES) throw IllegalStateException(zipEntryCountLimitExceededMessage(budget.entries, MAX_ZIP_ENTRIES))"
        "                    val name = entry.name"
        "                    if (!safeZipEntryName(name)) {"
        "                        throw IllegalStateException(\"Unsafe ZIP entry\")"
@@ -8063,7 +8085,7 @@
        "            val code = connection.responseCode"
        "            if (code !in 200..299) {"
        "                connection.errorStream?.close()"
-       "                throw IllegalStateException(\"HTTP $code\")"
+       "                throw IllegalStateException(httpStatusMessage(code))"
        "            }"
        "            if (connection.contentLengthLong > MAX_REMOTE_ZIP_BYTES) {"
        "                throw IllegalStateException(\"Remote ZIP exceeds compressed-byte limit\")"
@@ -8092,7 +8114,7 @@
        "            val code = connection.responseCode"
        "            if (code !in 200..299) {"
        "                connection.errorStream?.close()"
-       "                throw IllegalStateException(\"HTTP $code\")"
+       "                throw IllegalStateException(httpStatusMessage(code))"
        "            }"
        "                val response = readResponseText(connection)"
        "                return remoteDumpImportedCount(jsonObjectFromText(response))"
@@ -8221,7 +8243,7 @@
        "    private fun ensureTreeDir(parent: DocumentFile, name: String): DocumentFile ="
        "        parent.findFile(name.takeIf { safePathComponent(it) }"
        "            ?: throw IllegalArgumentException(\"Unsafe tree directory\"))?.takeIf { it.isDirectory } ?: parent.createDirectory(name)"
-       "            ?: throw IllegalStateException(\"Unable to create sync folder $name\")"
+       "            ?: throw IllegalStateException(unableToCreateSyncFolderMessage(name))"
        ""
        "    private fun writeTreeFile(parent: DocumentFile, relativeName: String, source: File) {"
        "        require(source.isFile && !Files.isSymbolicLink(source.toPath()))"
@@ -8234,7 +8256,7 @@
        "        dir.findFile(fileName)?.delete()"
        "        val mime = treeFileMime(fileName)"
        "        val doc = dir.createFile(mime, fileName)"
-       "            ?: throw IllegalStateException(\"Unable to create $fileName\")"
+       "            ?: throw IllegalStateException(unableToCreateFileMessage(fileName))"
        "        context.contentResolver.openOutputStream(doc.uri)?.use { output ->"
        "            source.inputStream().use { input ->"
        "                LimitedInputStream(input, MAX_ZIP_ENTRY_BYTES).use { it.copyTo(output, 32 * 1024) }"
diff --git a/tests/ssd-security-test.sh b/tests/ssd-security-test.sh
index 35c3eba..55e5362 100755
--- a/tests/ssd-security-test.sh
+++ b/tests/ssd-security-test.sh
@@ -39,7 +39,7 @@ reject_text() {
 require_text 'HttpsURLConnection'
 require_text 'X509TrustManager'
 require_text 'MessageDigest.isEqual'
-require_text 'setRequestProperty("Authorization", "Bearer ${config.bearerToken}")'
+require_text 'setRequestProperty("Authorization", bearerAuthorizationHeader(config.bearerToken))'
 require_text 'context.getSharedPreferences("ssd_review_remote", Context.MODE_PRIVATE)'
 require_text 'Remote SSD service is not locally configured'
 require_text 'validatedSourceKey'