Move SSD network safety checks to typed Kotlin

ober

c9b467e8808422a5c5d30e866bc37dfe6df890c8

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 30b0664..ce58759 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1424,7 +1424,8 @@
                 learnedExamplesFileName
                 inputLimitExceededMessage outputLimitExceededMessage
                 clientLogHttpStatusMessage bearerAuthorizationHeader
-                zipEntryCountLimitExceededMessage httpStatusSuccessful httpStatusMessage
+                zipEntryCountLimitExceededMessage
+                httpStatusSuccessful httpStatusFailed httpStatusMessage
                 unableToCreateSyncFolderMessage unableToCreateFileMessage)
         (type Int32)
         (record ImportBudget
@@ -1491,6 +1492,8 @@
               (string-append "/" (int32->string limit)))))
         (def (httpStatusSuccessful (code : Int32)) : Bool
           (and (>= code (int32 200)) (<= code (int32 299))))
+        (def (httpStatusFailed (code : Int32)) : Bool
+          (not (httpStatusSuccessful code)))
         (def (httpStatusMessage (code : Int32)) : String
           (string-append "HTTP " (int32->string code)))
         (def (unableToCreateSyncFolderMessage (name : String)) : String
@@ -1640,14 +1643,17 @@
                 TruthIndexEntry-sourceName TruthIndexEntry-canonicalSourceName
                 TruthIndexEntry-page TruthIndexEntry-dpi TruthIndexEntry-groupCount
                 TruthIndexEntry-truthTime betterTruthIndex
-                fileExists fileIsDirectory fileIsRegular fileIsSymbolicLink
+                fileExists fileIsDirectory fileIsRegular fileIsNotRegular
+                fileIsSymbolicLink
                 fileSizeBytes
-                pathIsSymbolicLink pathNotSymbolicLink safeLocalRegularFile
+                pathIsSymbolicLink pathNotSymbolicLink pathParentMatches
+                safeLocalRegularFile
                 destinationNewFileDelta
                 storageStatsExceedsQuota storageCapacityWithinQuota
                 byteCountWithinLimit storageEntryBytesAllowed
                 boundedReadLimitValid
-                positiveCount nonPositiveCount positiveByteCount learnedExamplesFull
+                positiveCount nonPositiveCount
+                positiveByteCount nonPositiveByteCount learnedExamplesFull
                 streamReadEnded streamReadEmpty streamByteWasRead
                 inputCountExceeded outputSingleWriteExceeds
                 outputBufferWriteExceeds
@@ -1658,7 +1664,8 @@
                 byteArraySizeBytes byteArrayBytesWithinLimit
                 textUtf8Bytes textUtf8SizeBytes textUtf8BytesWithinLimit
                 oldDestinationBytes urlEffectivePort
-                constantTimeBytesEqual constantTimeNullableBytesEqual
+                constantTimeBytesEqual constantTimeBytesMismatch
+                constantTimeNullableBytesEqual constantTimeNullableBytesMismatch
                 remoteBearerTokenPresent remoteBearerTokenValid
                 remoteEndpointIsHttps safeRemoteUrlParts
                 remotePinPresent remotePinOrNull remotePinSha256Length
@@ -1666,6 +1673,7 @@
                 remotePinAllowedForProtocol remoteConfigHasPin
                 remoteRelativePathSafe remoteOriginMatches
                 shouldCompareMoreCompleteTruth shouldWriteRemoteTruth
+                remoteTruthWriteSkipped
                 localFileMissing localTextMatchesRemote
                 shouldReplaceLocalByTimes
                 zipEntryIsDirectory zipEntryIsFile zipEntryDeclaresExcessiveSize)
@@ -1686,6 +1694,8 @@
           (kotlin-member-call toPath))
         (extern (pathIsSymbolicLinkRaw (path : Path)) : Bool
           (kotlin-call Files isSymbolicLink))
+        (extern (pathParent (path : Path)) : Path
+          (kotlin-member-get parent))
         (extern (bytesSize (bytes : Bytes)) : Int32
           (kotlin-member-get size))
         (extern (base64Decode (text : String) (flags : Int32)) : Bytes
@@ -1733,12 +1743,16 @@
           (and (fileExists file) (fileIsDirectoryRaw file)))
         (def (fileIsRegular (file : File)) : Bool
           (and (fileExists file) (fileIsFile file)))
+        (def (fileIsNotRegular (file : File)) : Bool
+          (not (fileIsRegular file)))
         (def (fileSizeBytes (file : File)) : Int
           (fileLength file))
         (def (pathIsSymbolicLink (path : Path)) : Bool
           (pathIsSymbolicLinkRaw path))
         (def (pathNotSymbolicLink (path : Path)) : Bool
           (not (pathIsSymbolicLink path)))
+        (def (pathParentMatches (path : Path) (parent : Path)) : Bool
+          (equal? (pathParent path) parent))
         (def (fileIsSymbolicLink (file : File)) : Bool
           (pathIsSymbolicLink (fileToPath file)))
         (def (safeLocalRegularFile (file : File)) : Bool
@@ -1772,6 +1786,8 @@
           (not (positiveCount count)))
         (def (positiveByteCount (count : Int)) : Bool
           (> count (int 0)))
+        (def (nonPositiveByteCount (count : Int)) : Bool
+          (not (positiveByteCount count)))
         (def (streamReadEnded (read : Int32)) : Bool
           (< read (int32 0)))
         (def (streamReadEmpty (read : Int32)) : Bool
@@ -1833,11 +1849,16 @@
             (if (>= port (int32 0)) port (urlDefaultPort url))))
         (def (constantTimeBytesEqual (actual : Bytes) (expected : Bytes)) : Bool
           (messageDigestIsEqual actual expected))
+        (def (constantTimeBytesMismatch (actual : Bytes) (expected : Bytes)) : Bool
+          (not (constantTimeBytesEqual actual expected)))
         (def (constantTimeNullableBytesEqual (actual : Bytes)
                                              (expected : (Nullable Bytes))) : Bool
           (if (nullable-null? expected)
               #f
               (constantTimeBytesEqual actual (nullable-get expected))))
+        (def (constantTimeNullableBytesMismatch (actual : Bytes)
+                                                (expected : (Nullable Bytes))) : Bool
+          (not (constantTimeNullableBytesEqual actual expected)))
         (def (remoteEndpointIsHttps (protocol : String) (host : String)) : Bool
           (and (equal? protocol "https") (not (string-blank? host))))
         (def (safeRemoteUrlParts (userInfo : (Nullable String))
@@ -1899,6 +1920,8 @@
           (if compareMoreComplete
             (or (>= remoteGroupCount localGroupCount) localPolicyAllows)
             localPolicyAllows))
+        (def (remoteTruthWriteSkipped (shouldWrite : Bool)) : Bool
+          (not shouldWrite))
         (def (shouldReplaceLocalByTimes (localGenerated : (Nullable Int))
                                         (remoteGenerated : (Nullable Int))
                                         (remoteModified : Int)
@@ -7437,7 +7460,7 @@
        "        try {"
        "            connection.outputStream.use { it.write(bytes) }"
        "            val code = connection.responseCode"
-       "            if (!httpStatusSuccessful(code)) throw IllegalStateException(clientLogHttpStatusMessage(code))"
+       "            if (httpStatusFailed(code)) throw IllegalStateException(clientLogHttpStatusMessage(code))"
        "            connection.inputStream.close()"
        "        } finally {"
        "            connection.disconnect()"
@@ -8338,7 +8361,7 @@
        "    }"
        ""
        "    private fun truthIndexEntry(file: File): TruthIndexEntry? {"
-       "        if (!fileIsRegular(file)) return null"
+       "        if (fileIsNotRegular(file)) return null"
        "        return try {"
        "            val truth = jsonObjectFromText(readLocalText(file))"
        "            truthIndexEntryFromJson(file, truth, truthTime(truth) ?: file.lastModified())"
@@ -8443,7 +8466,7 @@
        "        val basePath = base.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS)"
        "        require(pathNotSymbolicLink(basePath)) { \"Symlinked storage root rejected\" }"
        "        val target = basePath.resolve(leaf).normalize()"
-       "        require(target.parent == basePath) { \"Local path escaped storage root\" }"
+       "        require(pathParentMatches(target, basePath)) { \"Local path escaped storage root\" }"
        "        return target.toFile()"
        "    }"
        ""
@@ -8483,7 +8506,7 @@
        "        val parentPath = parent.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS)"
        "        require(pathNotSymbolicLink(parentPath)) { \"Symlinked destination parent rejected\" }"
        "        val target = parentPath.resolve(destination.name).normalize()"
-       "        require(target.parent == parentPath) { \"Destination escaped parent\" }"
+       "        require(pathParentMatches(target, parentPath)) { \"Destination escaped parent\" }"
        "        require(pathNotSymbolicLink(target)) { \"Symlinked destination rejected\" }"
        "        val temporary = Files.createTempFile(parentPath, \".ssd-review-\", \".part\")"
        "        try {"
@@ -8566,7 +8589,7 @@
        "            val certificate = connection.serverCertificates.firstOrNull()"
        "                ?: throw SSLPeerUnverifiedException(\"Server provided no certificate\")"
        "            val actualPin = MessageDigest.getInstance(\"SHA-256\").digest(certificate.publicKey.encoded)"
-       "            if (!constantTimeNullableBytesEqual(actualPin, config.spkiSha256)) {"
+       "            if (constantTimeNullableBytesMismatch(actualPin, config.spkiSha256)) {"
        "                connection.disconnect()"
        "                throw SSLPeerUnverifiedException(\"SSD service identity pin mismatch\")"
        "            }"
@@ -8590,7 +8613,7 @@
        "                platform.checkServerTrusted(chain, authType)"
        "                val leaf = chain.firstOrNull() ?: throw CertificateException(\"Server provided no certificate\")"
        "                val actual = MessageDigest.getInstance(\"SHA-256\").digest(leaf.publicKey.encoded)"
-       "                if (!constantTimeBytesEqual(actual, expectedPin)) {"
+       "                if (constantTimeBytesMismatch(actual, expectedPin)) {"
        "                    throw CertificateException(\"SSD service identity pin mismatch\")"
        "                }"
        "            }"
@@ -8606,7 +8629,7 @@
        "            val connection = openPinnedConnection(relative, \"GET\", acceptGzip = true)"
        "            try {"
        "            val code = connection.responseCode"
-       "            if (!httpStatusSuccessful(code)) {"
+       "            if (httpStatusFailed(code)) {"
        "                connection.errorStream?.close()"
        "                return 0"
        "            }"
@@ -8672,7 +8695,7 @@
        "        val compareMoreComplete = shouldCompareMoreCompleteTruth(replaceIfMoreComplete, dest)"
        "        val local = if (compareMoreComplete) jsonObjectFromText(readLocalText(dest)) else null"
        "        val shouldWrite = shouldWriteRemoteTruth(compareMoreComplete, truthGroupCount(truth), nullableTruthGroupCount(local), shouldReplaceLocal(dest, text, modified))"
-       "        if (!shouldWrite) return false"
+       "        if (remoteTruthWriteSkipped(shouldWrite)) return false"
        "        atomicWriteText(dest, text)"
        "        invalidateTruthIndex()"
        "        return true"
@@ -8829,7 +8852,7 @@
        "        val connection = openPinnedConnection(\"dump.zip?scope=app\", \"GET\")"
        "        try {"
        "            val code = connection.responseCode"
-       "            if (!httpStatusSuccessful(code)) {"
+       "            if (httpStatusFailed(code)) {"
        "                connection.errorStream?.close()"
        "                throw IllegalStateException(httpStatusMessage(code))"
        "            }"
@@ -8858,7 +8881,7 @@
        "                    archive.inputStream().use { input -> input.copyTo(output, 32 * 1024) }"
        "                }"
        "            val code = connection.responseCode"
-       "            if (!httpStatusSuccessful(code)) {"
+       "            if (httpStatusFailed(code)) {"
        "                connection.errorStream?.close()"
        "                throw IllegalStateException(httpStatusMessage(code))"
        "            }"
@@ -8966,7 +8989,7 @@
        "                val name = child.name ?: return@forEach"
        "                if (safeLeafInvalid(name)) return@forEach"
        "                val remaining = MAX_ZIP_TOTAL_BYTES - budget.expandedBytes"
-       "                if (!positiveByteCount(remaining)) throw IllegalStateException(\"Import tree exceeds byte limit\")"
+       "                if (nonPositiveByteCount(remaining)) throw IllegalStateException(\"Import tree exceeds byte limit\")"
        "                val bytes = context.contentResolver.openInputStream(child.uri)?.use {"
        "                    readBoundedBytes(it, boundedTreeReadLimit(MAX_ZIP_ENTRY_BYTES, remaining))"
        "                }"
@@ -9075,7 +9098,7 @@
        "                try {"
        "                    connection.outputStream.use { it.write(bytes) }"
        "                    val code = connection.responseCode"
-       "                    if (!httpStatusSuccessful(code)) {"
+       "                    if (httpStatusFailed(code)) {"
        "                        connection.errorStream?.close()"
        "                    } else {"
        "                        connection.inputStream?.close()"