Use typed byte size helpers in SSD storage

ober

0fd4ba01343c63f0764437362a3604d89053c2d7

diff --git a/.build.yml b/.build.yml
index 35ec0f9..68f4868 100644
--- a/.build.yml
+++ b/.build.yml
@@ -5,7 +5,7 @@ packages:
   - make=4.4.1-r4
 sources:
   # Build dependency: full immutable commit, mirrored in dependencies.lock.json.
-  - "https://git.sr.ht/~lisp/jerboa#b98aa23ec2a473d30bdd71e5c9681c9ec16318de"
+  - "https://git.sr.ht/~lisp/jerboa#45689469df19c42f5f282439c3aa356ac96a5021"
   # The second source is the build subject selected by the SourceHut submitter.
   - https://git.sr.ht/~lisp/jerboa-android
 tasks:
@@ -14,6 +14,6 @@ tasks:
       test "$(apk info -v chez-scheme)" = chez-scheme-10.3.0-r2
       test "$(apk info -v git)" = git-2.54.0-r0
       test "$(apk info -v make)" = make-4.4.1-r4
-      test "$(git -C ../jerboa rev-parse HEAD)" = b98aa23ec2a473d30bdd71e5c9681c9ec16318de
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = 18d86823fcb7a2aa532056bc51fa3df8dbebdc48
+      test "$(git -C ../jerboa rev-parse HEAD)" = 45689469df19c42f5f282439c3aa356ac96a5021
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = a35a87a4e3f83550eb1bcd5b5eef518c72321a6a
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index 116387d..fac1dd6 100644
--- a/dependencies.lock.json
+++ b/dependencies.lock.json
@@ -11,8 +11,8 @@
   "generator_runtime": {
     "name": "jerboa",
     "repository": "https://git.sr.ht/~lisp/jerboa",
-    "commit": "b98aa23ec2a473d30bdd71e5c9681c9ec16318de",
-    "tree": "18d86823fcb7a2aa532056bc51fa3df8dbebdc48"
+    "commit": "45689469df19c42f5f282439c3aa356ac96a5021",
+    "tree": "a35a87a4e3f83550eb1bcd5b5eef518c72321a6a"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index ab32198..2c8821f 100755
--- a/scripts/verify-supply-chain.sh
+++ b/scripts/verify-supply-chain.sh
@@ -3,8 +3,8 @@ set -eu
 
 repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
 lock="$repo/dependencies.lock.json"
-jerboa_commit=b98aa23ec2a473d30bdd71e5c9681c9ec16318de # gitsafe:ignore
-jerboa_tree=18d86823fcb7a2aa532056bc51fa3df8dbebdc48 # gitsafe:ignore
+jerboa_commit=45689469df19c42f5f282439c3aa356ac96a5021 # gitsafe:ignore
+jerboa_tree=a35a87a4e3f83550eb1bcd5b5eef518c72321a6a # gitsafe:ignore
 gradle_sha=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
 jdk_macos_sha=8fa1eff40bb637a33613b2ccb8b12c70dc3661cc22cf8e784943715769a05336
 jdk_linux_sha=d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331
diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index fdcbe62..822c20a 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1580,6 +1580,8 @@
                 storageStatsExceedsQuota storageCapacityWithinQuota
                 storageEntryBytesAllowed zipExportQuotaExceeded
                 treeExportQuotaExceeded
+                byteArraySizeBytes byteArrayBytesWithinLimit
+                textUtf8SizeBytes textUtf8BytesWithinLimit
                 oldDestinationBytes urlEffectivePort
                 remoteBearerTokenPresent remoteBearerTokenValid
                 remotePinPresent remotePinOrNull remotePinSha256Length
@@ -1687,6 +1689,14 @@
                                       (maxEntries : Int32)
                                       (maxTotalBytes : Int)) : Bool
           (or (> entries maxEntries) (> bytes maxTotalBytes)))
+        (def (byteArraySizeBytes (bytes : Bytes)) : Int
+          (int (bytevector-length bytes)))
+        (def (byteArrayBytesWithinLimit (bytes : Bytes) (maxBytes : Int)) : Bool
+          (<= (byteArraySizeBytes bytes) maxBytes))
+        (def (textUtf8SizeBytes (text : String)) : Int
+          (byteArraySizeBytes (string->utf8 text)))
+        (def (textUtf8BytesWithinLimit (text : String) (maxBytes : Int)) : Bool
+          (<= (textUtf8SizeBytes text) maxBytes))
         (def (urlEffectivePort (url : URL)) : Int32
           (let ((port (urlPort url)))
             (if (>= port (int32 0)) port (urlDefaultPort url))))
@@ -8141,7 +8151,7 @@
        "    }"
        ""
        "    private fun atomicWrite(destination: File, bytes: ByteArray) {"
-       "        ensureStorageCapacity(destination, bytes.size.toLong())"
+       "        ensureStorageCapacity(destination, byteArraySizeBytes(bytes))"
        "        val parent = destination.parentFile ?: throw IllegalArgumentException(\"Destination has no parent\")"
        "        val parentPath = parent.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS)"
        "        require(!pathIsSymbolicLink(parentPath)) { \"Symlinked destination parent rejected\" }"
@@ -8638,7 +8648,7 @@
        "                    readBoundedBytes(it, boundedTreeReadLimit(MAX_ZIP_ENTRY_BYTES, remaining))"
        "                }"
        "                    ?: return@forEach"
-       "                budget.expandedBytes += bytes.size.toLong()"
+       "                budget.expandedBytes += byteArraySizeBytes(bytes)"
        "                validateImportedText(name, bytes.decodeToString())"
        "                val dest = containedImportPath(root.toPath().relativize(File(localDir, name).toPath()).toString())"
        "                if (jsonLinesFileName(name)) {"
@@ -8678,7 +8688,7 @@
        "    }"
        ""
        "    private fun mergeJsonLines(file: File, remoteText: String) {"
-       "        require(remoteText.toByteArray(Charsets.UTF_8).size.toLong() <= MAX_ZIP_ENTRY_BYTES)"
+       "        require(textUtf8BytesWithinLimit(remoteText, MAX_ZIP_ENTRY_BYTES))"
        "        validateImportedText(file.name, remoteText)"
        "        val lines = linkedSetOf<String>()"
        "        if (fileExists(file)) {"
@@ -8689,7 +8699,7 @@
        "            lines.add(it)"
        "        }"
        "        val merged = mergedJsonLinesText(lines)"
-       "        require(merged.toByteArray(Charsets.UTF_8).size.toLong() <= MAX_ZIP_ENTRY_BYTES)"
+       "        require(textUtf8BytesWithinLimit(merged, MAX_ZIP_ENTRY_BYTES))"
        "        atomicWriteText(file, merged)"
        "    }"
        ""
@@ -8726,7 +8736,7 @@
        "    private fun appendJsonLine(file: File, json: JSONObject) {"
        "        val previous = if (fileExists(file)) readLocalText(file) else \"\""
        "        val next = previous + json.toString() + \"\\n\""
-       "        require(next.toByteArray(Charsets.UTF_8).size.toLong() <= MAX_ZIP_ENTRY_BYTES) { \"Event log quota exceeded\" }"
+       "        require(textUtf8BytesWithinLimit(next, MAX_ZIP_ENTRY_BYTES)) { \"Event log quota exceeded\" }"
        "        atomicWriteText(file, next)"
        "    }"
        ""
@@ -8735,11 +8745,11 @@
        "        thread(name = \"sfb-truth-autosave\") {"
        "            try {"
        "                val bytes = payload.toByteArray(Charsets.UTF_8)"
-       "                require(bytes.size.toLong() <= MAX_HTTP_TEXT_BYTES) { \"Autosave payload exceeds byte limit\" }"
+       "                require(byteArrayBytesWithinLimit(bytes, MAX_HTTP_TEXT_BYTES)) { \"Autosave payload exceeds byte limit\" }"
        "                val connection = openPinnedConnection("
        "                    \"truth\","
        "                    \"POST\","
-       "                    outputBytes = bytes.size.toLong(),"
+       "                    outputBytes = byteArraySizeBytes(bytes),"
        "                    contentType = \"application/json; charset=utf-8\""
        "                )"
        "                try {"