Move SSD quota checks to typed Kotlin

ober

7d2485b7528b0e492c38109408a323b3375ef1c2

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 13ac92d..fdcbe62 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1577,6 +1577,9 @@
                 fileSizeBytes
                 pathIsSymbolicLink safeLocalRegularFile
                 destinationNewFileDelta
+                storageStatsExceedsQuota storageCapacityWithinQuota
+                storageEntryBytesAllowed zipExportQuotaExceeded
+                treeExportQuotaExceeded
                 oldDestinationBytes urlEffectivePort
                 remoteBearerTokenPresent remoteBearerTokenValid
                 remotePinPresent remotePinOrNull remotePinSha256Length
@@ -1658,6 +1661,32 @@
           (if (fileExists destination) (int32 0) (int32 1)))
         (def (oldDestinationBytes (destination : File)) : Int
           (if (fileIsFile destination) (fileLength destination) (int 0)))
+        (def (storageStatsExceedsQuota (files : Int32)
+                                        (bytes : Int)
+                                        (maxFiles : Int32)
+                                        (maxBytes : Int)) : Bool
+          (or (> files maxFiles) (> bytes maxBytes)))
+        (def (storageCapacityWithinQuota (files : Int32)
+                                         (bytes : Int)
+                                         (maxFiles : Int32)
+                                         (maxBytes : Int)) : Bool
+          (and (<= files maxFiles) (<= bytes maxBytes)))
+        (def (storageEntryBytesAllowed (bytes : Int) (maxBytes : Int)) : Bool
+          (and (>= bytes (int 0)) (<= bytes maxBytes)))
+        (def (zipExportQuotaExceeded (entries : Int32)
+                                     (fileBytes : Int)
+                                     (sourceBytes : Int)
+                                     (maxEntries : Int32)
+                                     (maxEntryBytes : Int)
+                                     (maxTotalBytes : Int)) : Bool
+          (or (> entries maxEntries)
+              (or (> fileBytes maxEntryBytes)
+                  (> sourceBytes maxTotalBytes))))
+        (def (treeExportQuotaExceeded (entries : Int32)
+                                      (bytes : Int)
+                                      (maxEntries : Int32)
+                                      (maxTotalBytes : Int)) : Bool
+          (or (> entries maxEntries) (> bytes maxTotalBytes)))
         (def (urlEffectivePort (url : URL)) : Int32
           (let ((port (urlPort url)))
             (if (>= port (int32 0)) port (urlDefaultPort url))))
@@ -8091,7 +8120,7 @@
        "            } else if (fileIsRegular(file)) {"
        "                stats.files += 1"
        "                stats.bytes += fileSizeBytes(file)"
-       "                if (stats.files > MAX_STORAGE_FILES || stats.bytes > MAX_STORAGE_BYTES) {"
+       "                if (storageStatsExceedsQuota(stats.files, stats.bytes, MAX_STORAGE_FILES, MAX_STORAGE_BYTES)) {"
        "                    throw IllegalStateException(\"Storage quota exceeded\")"
        "                }"
        "            }"
@@ -8101,12 +8130,12 @@
        "    }"
        ""
        "    private fun ensureStorageCapacity(destination: File, bytes: Long) {"
-       "        require(bytes in 0..MAX_ZIP_ENTRY_BYTES) { \"Per-file storage limit exceeded\" }"
+       "        require(storageEntryBytesAllowed(bytes, MAX_ZIP_ENTRY_BYTES)) { \"Per-file storage limit exceeded\" }"
        "        val stats = storageStats()"
        "        val oldBytes = oldDestinationBytes(destination)"
        "        val newFiles = stats.files + destinationNewFileDelta(destination)"
        "        val newBytes = stats.bytes - oldBytes + bytes"
-       "        require(newFiles <= MAX_STORAGE_FILES && newBytes <= MAX_STORAGE_BYTES) {"
+       "        require(storageCapacityWithinQuota(newFiles, newBytes, MAX_STORAGE_FILES, MAX_STORAGE_BYTES)) {"
        "            \"Storage quota exceeded\""
        "        }"
        "    }"
@@ -8334,7 +8363,7 @@
        "                    if (fileIsSymbolicLink(file)) throw IllegalStateException(\"Storage symlink rejected\")"
        "                    entries += 1"
        "                    sourceBytes += fileSizeBytes(file)"
-       "                    if (entries > MAX_ZIP_ENTRIES || fileSizeBytes(file) > MAX_ZIP_ENTRY_BYTES || sourceBytes > MAX_ZIP_TOTAL_BYTES) {"
+       "                    if (zipExportQuotaExceeded(entries, fileSizeBytes(file), sourceBytes, MAX_ZIP_ENTRIES, MAX_ZIP_ENTRY_BYTES, MAX_ZIP_TOTAL_BYTES)) {"
        "                        throw IllegalStateException(\"ZIP export quota exceeded\")"
        "                    }"
        "                    val relative = root.toPath().relativize(file.toPath()).toString()"
@@ -8561,7 +8590,7 @@
        "            dir.walkTopDown().filter { fileIsRegular(it) }.forEach { file ->"
        "                count += 1"
        "                bytes += fileSizeBytes(file)"
-       "                if (count > MAX_ZIP_ENTRIES || bytes > MAX_ZIP_TOTAL_BYTES) {"
+       "                if (treeExportQuotaExceeded(count, bytes, MAX_ZIP_ENTRIES, MAX_ZIP_TOTAL_BYTES)) {"
        "                    throw IllegalStateException(\"Tree export quota exceeded\")"
        "                }"
        "                val relative = dir.toPath().relativize(file.toPath()).toString()"