Move SSD ZIP limit checks to typed Kotlin

ober

9a039a4c8265b7c09a0ab8368bc544d1c7f998cb

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 0a4f3de..b8671fc 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1578,7 +1578,10 @@
                 pathIsSymbolicLink safeLocalRegularFile
                 destinationNewFileDelta
                 storageStatsExceedsQuota storageCapacityWithinQuota
-                storageEntryBytesAllowed zipExportQuotaExceeded
+                byteCountWithinLimit storageEntryBytesAllowed
+                positiveCount learnedExamplesShouldRebuild
+                zipEntryCountExceeded zipExpandedBytesExceeded
+                zipCompressionRatioExceeded zipExportQuotaExceeded
                 treeExportQuotaExceeded
                 byteArraySizeBytes byteArrayBytesWithinLimit
                 textUtf8SizeBytes textUtf8BytesWithinLimit
@@ -1674,8 +1677,26 @@
                                          (maxFiles : Int32)
                                          (maxBytes : Int)) : Bool
           (and (<= files maxFiles) (<= bytes maxBytes)))
-        (def (storageEntryBytesAllowed (bytes : Int) (maxBytes : Int)) : Bool
+        (def (byteCountWithinLimit (bytes : Int) (maxBytes : Int)) : Bool
           (and (>= bytes (int 0)) (<= bytes maxBytes)))
+        (def (storageEntryBytesAllowed (bytes : Int) (maxBytes : Int)) : Bool
+          (byteCountWithinLimit bytes maxBytes))
+        (def (positiveCount (count : Int32)) : Bool
+          (> count (int32 0)))
+        (def (learnedExamplesShouldRebuild (imported : Int32)
+                                           (learnedFile : File)) : Bool
+          (or (positiveCount imported) (not (fileExists learnedFile))))
+        (def (zipEntryCountExceeded (entries : Int32) (maxEntries : Int32)) : Bool
+          (> entries maxEntries))
+        (def (zipExpandedBytesExceeded (entryBytes : Int)
+                                       (expandedBytes : Int)
+                                       (maxEntryBytes : Int)
+                                       (maxTotalBytes : Int)) : Bool
+          (or (> entryBytes maxEntryBytes)
+              (> expandedBytes maxTotalBytes)))
+        (def (zipCompressionRatioExceeded (expandedBytes : Int)
+                                          (ratioBudget : Int)) : Bool
+          (> expandedBytes ratioBudget))
         (def (zipExportQuotaExceeded (entries : Int32)
                                      (fileBytes : Int)
                                      (sourceBytes : Int)
@@ -8246,7 +8267,7 @@
        "            if (acceptGzip) setRequestProperty(\"Accept-Encoding\", \"gzip\")"
        "            if (contentType != null) setRequestProperty(\"Content-Type\", contentType)"
        "            if (outputBytes != null) {"
-       "                require(outputBytes in 0..MAX_ZIP_TOTAL_BYTES) { \"Upload exceeds byte limit\" }"
+       "                require(byteCountWithinLimit(outputBytes, MAX_ZIP_TOTAL_BYTES)) { \"Upload exceeds byte limit\" }"
        "                doOutput = true"
        "                setFixedLengthStreamingMode(outputBytes)"
        "            }"
@@ -8413,7 +8434,7 @@
        "                while (true) {"
        "                    val entry = zip.nextEntry ?: break"
        "                    budget.entries += 1"
-       "                    if (budget.entries > MAX_ZIP_ENTRIES) throw IllegalStateException(zipEntryCountLimitExceededMessage(budget.entries, MAX_ZIP_ENTRIES))"
+       "                    if (zipEntryCountExceeded(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\")"
@@ -8431,14 +8452,13 @@
        "                }"
        "            }"
        "            val current = storageStats()"
-       "            require(current.files + staged.size <= MAX_STORAGE_FILES) { \"Storage file-count quota exceeded\" }"
-       "            require(current.bytes + budget.expandedBytes <= MAX_STORAGE_BYTES) { \"Storage byte quota exceeded\" }"
+       "            require(storageCapacityWithinQuota(current.files + staged.size, current.bytes + budget.expandedBytes, MAX_STORAGE_FILES, MAX_STORAGE_BYTES)) { \"Storage quota exceeded\" }"
        "            staged.forEach { if (installStagedZipEntry(it)) count += 1 }"
-       "            if (count > 0) invalidateTruthIndex()"
+       "            if (positiveCount(count)) invalidateTruthIndex()"
        "        } finally {"
        "            stage.deleteRecursively()"
        "        }"
-       "        if (count > 0 || !fileExists(File(learnedDir, learnedExamplesFileName()))) {"
+       "        if (learnedExamplesShouldRebuild(count, File(learnedDir, learnedExamplesFileName()))) {"
        "            rebuildLearnedExamples()"
        "        }"
        "        return count"
@@ -8459,11 +8479,11 @@
        "                if (read == 0) continue"
        "                entryBytes += read.toLong()"
        "                budget.expandedBytes += read.toLong()"
-       "                if (entryBytes > MAX_ZIP_ENTRY_BYTES || budget.expandedBytes > MAX_ZIP_TOTAL_BYTES) {"
+       "                if (zipExpandedBytesExceeded(entryBytes, budget.expandedBytes, MAX_ZIP_ENTRY_BYTES, MAX_ZIP_TOTAL_BYTES)) {"
        "                    throw IllegalStateException(\"ZIP expanded-byte limit exceeded\")"
        "                }"
        "                val ratioBudget = compressionRatioBudget(RATIO_FLOOR_BYTES, compressed.count, MAX_COMPRESSION_RATIO)"
-       "                if (budget.expandedBytes > ratioBudget) {"
+       "                if (zipCompressionRatioExceeded(budget.expandedBytes, ratioBudget)) {"
        "                    throw IllegalStateException(\"ZIP compression-ratio limit exceeded\")"
        "                }"
        "                output.write(buffer, 0, read)"
@@ -8537,11 +8557,11 @@
        "        val archive = File.createTempFile(\"ssd-upload-\", \".zip\", context.cacheDir)"
        "        try {"
        "            archive.outputStream().use { exportZip(it) }"
-       "            require(archive.length() <= MAX_ZIP_TOTAL_BYTES) { \"Upload ZIP exceeds byte limit\" }"
+       "            require(byteCountWithinLimit(fileSizeBytes(archive), MAX_ZIP_TOTAL_BYTES)) { \"Upload ZIP exceeds byte limit\" }"
        "            val connection = openPinnedConnection("
        "                \"load.zip\","
        "                \"POST\","
-       "                outputBytes = archive.length(),"
+       "                outputBytes = fileSizeBytes(archive),"
        "                contentType = \"application/zip\""
        "            )"
        "            try {"
@@ -8648,7 +8668,7 @@
        "        var count = 0"
        "        sourceDir.listFiles().forEach { child ->"
        "            budget.entries += 1"
-       "            if (budget.entries > MAX_ZIP_ENTRIES) throw IllegalStateException(\"Import tree exceeds entry limit\")"
+       "            if (zipEntryCountExceeded(budget.entries, MAX_ZIP_ENTRIES)) throw IllegalStateException(\"Import tree exceeds entry limit\")"
        "            if (documentFileIsDirectory(child)) {"
        "                val directoryName = child.name ?: return@forEach"
        "                if (!safePathComponent(directoryName)) throw IllegalStateException(\"Unsafe import directory\")"
@@ -8684,7 +8704,7 @@
        ""
        "    private fun writeTreeFile(parent: DocumentFile, relativeName: String, source: File) {"
        "        require(safeLocalRegularFile(source))"
-       "        require(fileSizeBytes(source) <= MAX_ZIP_ENTRY_BYTES) { \"Tree export file exceeds byte limit\" }"
+       "        require(byteCountWithinLimit(fileSizeBytes(source), MAX_ZIP_ENTRY_BYTES)) { \"Tree export file exceeds byte limit\" }"
        "        val parts = trimmedNonBlankPathParts(relativeName, File.separatorChar, '/')"
        "        require(safeTreePathParts(parts, MAX_ZIP_DEPTH))"
        "        val fileName = treePathLeaf(parts)"