Generate SSD truth store storage accounting from typed Jerboa

ober

e2a28216871b513244abec1ae1652e17a844582f

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index a69924d..e77d984 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -15539,7 +15539,9 @@
                 truthStoreReadLocalText truthStoreTruthPath
                 truthStoreLoadTruth truthStoreSaveSession
                 truthStoreAppendEvent truthStoreApplyTruth
-                truthStoreStorageSummaryLocal)
+                truthStoreStorageSummaryLocal
+                truthStoreStorageStatsLocal
+                truthStoreEnsureStorageCapacityLocal)
         (type Exception)
         (type File)
         (type InputStream)
@@ -15548,8 +15550,12 @@
         (type MessageDigest)
         (type Path)
         (type SsdSession)
+        (type StorageStats)
         (type TruthStore)
         (type JSONObject)
+        (extern (truthStoreRoot
+                  (store : TruthStore)) : File
+          (kotlin-member-get root))
         (extern (truthStoreGroundTruthDir
                   (store : TruthStore)) : File
           (kotlin-member-get groundTruthDir))
@@ -15603,6 +15609,9 @@
         (extern (truthStoreIllegalArgumentException
                   (message : String)) : Exception
           (kotlin-call IllegalArgumentException))
+        (extern (truthStoreIllegalStateException
+                  (message : String)) : Exception
+          (kotlin-call IllegalStateException))
         (extern (truthStoreMessageDigestGetInstance
                   (algorithm : String)) : MessageDigest
           (kotlin-call MessageDigest getInstance))
@@ -15612,6 +15621,10 @@
           (kotlin-member-call digest))
         (extern (truthStoreMaxZipEntryBytes) : Int
           (kotlin-value MAX_ZIP_ENTRY_BYTES))
+        (extern (truthStoreMaxStorageBytes) : Int
+          (kotlin-value MAX_STORAGE_BYTES))
+        (extern (truthStoreMaxStorageFiles) : Int32
+          (kotlin-value MAX_STORAGE_FILES))
         (def (truthStoreSha256Hex (bytes : Bytes)) : String
           (bytesToLowerHex
             (truthStoreMessageDigestDigest
@@ -15742,7 +15755,90 @@
             (truthStoreCountTruthFiles
               (truthStoreListFiles (truthStoreGroundTruthDir store)))
             (truthStoreCountFiles
-              (truthStoreListFiles (truthStoreEventsDir store)))))))
+              (truthStoreListFiles (truthStoreEventsDir store)))))
+        (def (truthStoreAccumulateStorageStats!
+               (file : File)
+               (stats : StorageStats)
+               (depth : Int32)) : Unit
+          (begin
+            (if (treeDepthExceeded depth (int32 32))
+              (throw
+                (truthStoreIllegalStateException "Storage tree too deep")
+                Unit)
+              (begin))
+            (if (fileIsSymbolicLink file)
+              (throw
+                (truthStoreIllegalStateException "Storage symlink rejected")
+                Unit)
+              (begin))
+            (if (fileIsDirectory file)
+              (let ((children (truthStoreListFiles file)))
+                (if (nullable-null? children)
+                  (begin)
+                  (forEachFileArray
+                    (nullable-get children)
+                    (lambda ((child : File))
+                      (truthStoreAccumulateStorageStats!
+                        child
+                        stats
+                        (+ depth (int32 1)))))))
+              (if (fileIsRegular file)
+                (begin
+                  (StorageStats-files-set!
+                    stats
+                    (+ (StorageStats-files stats) (int32 1)))
+                  (StorageStats-bytes-set!
+                    stats
+                    (+ (StorageStats-bytes stats) (fileSizeBytes file)))
+                  (if (storageStatsExceedsQuota
+                        (StorageStats-files stats)
+                        (StorageStats-bytes stats)
+                        (truthStoreMaxStorageFiles)
+                        (truthStoreMaxStorageBytes))
+                    (throw
+                      (truthStoreIllegalStateException
+                        "Storage quota exceeded")
+                      Unit)
+                    (begin)))
+                (begin)))))
+        (def (truthStoreStorageStatsLocal (store : TruthStore)) : StorageStats
+          (let ((stats (make-StorageStats (int32 0) (int 0))))
+            (begin
+              (truthStoreAccumulateStorageStats!
+                (truthStoreRoot store)
+                stats
+                (int32 0))
+              stats)))
+        (def (truthStoreEnsureStorageCapacityLocal
+               (store : TruthStore)
+               (destination : File)
+               (bytes : Int)) : Unit
+          (begin
+            (if (storageEntryBytesAllowed
+                  bytes
+                  (truthStoreMaxZipEntryBytes))
+              (begin)
+              (throw
+                (truthStoreIllegalArgumentException
+                  "Per-file storage limit exceeded")
+                Unit))
+            (let ((stats (truthStoreStorageStatsLocal store)))
+              (let ((oldBytes (oldDestinationBytes destination))
+                    (newFiles
+                      (+ (StorageStats-files stats)
+                         (destinationNewFileDelta destination))))
+                (let ((newBytes
+                        (+ (- (StorageStats-bytes stats) oldBytes) bytes)))
+                  (if (storageCapacityWithinQuota
+                        newFiles
+                        newBytes
+                        (truthStoreMaxStorageFiles)
+                        (truthStoreMaxStorageBytes))
+                    (begin)
+                    (throw
+                      (truthStoreIllegalArgumentException
+                        "Storage quota exceeded")
+                      Unit)))))))))
 
     (kotlin-file-lines "com/sfb/ssdreview/TruthStore.kt"
       (
@@ -15789,7 +15885,7 @@
        "import kotlin.concurrent.thread"
        ""
        "class TruthStore(private val context: Context) {"
-       "    private val root = File(context.filesDir, \"ssd_review\")"
+       "    internal val root = File(context.filesDir, \"ssd_review\")"
        "    internal val groundTruthDir = File(root, \"ground_truth\")"
        "    private val learnedDir = File(root, \"learned\")"
        "    internal val eventsDir = File(root, \"events\")"
@@ -15884,39 +15980,11 @@
        "    private fun containedLeaf(base: File, leaf: String): File ="
        "        truthStoreContainedLeaf(base, leaf)"
        ""
-       "    private fun storageStats(): StorageStats {"
-       "        val stats = StorageStats(0, 0L)"
-       "        fun walk(file: File, depth: Int) {"
-       "            if (treeDepthExceeded(depth, 32)) throw IllegalStateException(\"Storage tree too deep\")"
-       "            if (fileIsSymbolicLink(file)) throw IllegalStateException(\"Storage symlink rejected\")"
-       "            if (fileIsDirectory(file)) {"
-       "                if (fileListFilesPresent(file)) {"
-       "                    forEachFileArray(checkNotNull(file.listFiles())) { child ->"
-       "                        walk(child, depth + 1)"
-       "                    }"
-       "                }"
-       "            } else if (fileIsRegular(file)) {"
-       "                stats.files += 1"
-       "                stats.bytes += fileSizeBytes(file)"
-       "                if (storageStatsExceedsQuota(stats.files, stats.bytes, MAX_STORAGE_FILES, MAX_STORAGE_BYTES)) {"
-       "                    throw IllegalStateException(\"Storage quota exceeded\")"
-       "                }"
-       "            }"
-       "        }"
-       "        walk(root, 0)"
-       "        return stats"
-       "    }"
+       "    private fun storageStats(): StorageStats ="
+       "        truthStoreStorageStatsLocal(this)"
        ""
-       "    private fun ensureStorageCapacity(destination: File, bytes: Long) {"
-       "        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(storageCapacityWithinQuota(newFiles, newBytes, MAX_STORAGE_FILES, MAX_STORAGE_BYTES)) {"
-       "            \"Storage quota exceeded\""
-       "        }"
-       "    }"
+       "    private fun ensureStorageCapacity(destination: File, bytes: Long) ="
+       "        truthStoreEnsureStorageCapacityLocal(this, destination, bytes)"
        ""
        "    private fun atomicWrite(destination: File, bytes: ByteArray) {"
        "        ensureStorageCapacity(destination, byteArraySizeBytes(bytes))"