Move SSD nullable network checks to typed Kotlin

ober

261710950e03682f54ef49b3a906c59d2197563b

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index d87eebf..8308bf6 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1679,6 +1679,9 @@
                 treeExportQuotaExceeded
                 byteArraySizeBytes byteArrayBytesWithinLimit
                 textUtf8Bytes textUtf8SizeBytes textUtf8BytesWithinLimit
+                nullableTextPresent nullableTextOrEmpty
+                nullableByteCountPresent nullableByteCountOrZero
+                nullableByteCountWithinLimit
                 oldDestinationBytes urlEffectivePort
                 constantTimeBytesEqual constantTimeBytesMismatch
                 constantTimeNullableBytesEqual constantTimeNullableBytesMismatch
@@ -1794,6 +1797,17 @@
           (and (<= files maxFiles) (<= bytes maxBytes)))
         (def (byteCountWithinLimit (bytes : Int) (maxBytes : Int)) : Bool
           (and (>= bytes (int 0)) (<= bytes maxBytes)))
+        (def (nullableTextPresent (value : (Nullable String))) : Bool
+          (not (nullable-null? value)))
+        (def (nullableTextOrEmpty (value : (Nullable String))) : String
+          (if (nullable-null? value) "" (nullable-get value)))
+        (def (nullableByteCountPresent (bytes : (Nullable Int))) : Bool
+          (not (nullable-null? bytes)))
+        (def (nullableByteCountOrZero (bytes : (Nullable Int))) : Int
+          (if (nullable-null? bytes) (int 0) (nullable-get bytes)))
+        (def (nullableByteCountWithinLimit (bytes : (Nullable Int))
+                                           (maxBytes : Int)) : Bool
+          (byteCountWithinLimit (nullableByteCountOrZero bytes) maxBytes))
         (def (storageEntryBytesAllowed (bytes : Int) (maxBytes : Int)) : Bool
           (byteCountWithinLimit bytes maxBytes))
         (def (boundedReadLimitValid (limit : Int)) : Bool
@@ -5566,6 +5580,7 @@
       (typed-library (com sfb ssdreview)
         (export bestRemoteTruthForSession remoteTruthTimestamp
                 remoteTruthPayloadTruth remoteTruthPayloadMatchModified
+                remoteTruthPayloadHasTruth remoteTruthPayloadTruthOrEmpty
                 remoteTruthPayloadCount remoteTruthPayloadTruths
                 remoteTruthEntriesCount remoteTruthEntryAt
                 remoteTruthEntryTruth remoteTruthEntryModified
@@ -5581,6 +5596,10 @@
               generated)))
         (def (remoteTruthPayloadTruth (payload : JSONObject)) : (Nullable JSONObject)
           (json-object-opt-json-object payload "truth"))
+        (def (remoteTruthPayloadHasTruth (truth : (Nullable JSONObject))) : Bool
+          (not (nullable-null? truth)))
+        (def (remoteTruthPayloadTruthOrEmpty (truth : (Nullable JSONObject))) : JSONObject
+          (if (nullable-null? truth) (json-object-empty) (nullable-get truth)))
         (def (remoteTruthPayloadMatchModified (payload : JSONObject)) : Int
           (let ((match (json-object-opt-json-object payload "match")))
             (if (nullable-null? match)
@@ -8611,11 +8630,11 @@
        "            if (remoteBearerTokenPresent(config.bearerToken)) setRequestProperty(\"Authorization\", bearerAuthorizationHeader(config.bearerToken))"
        "            setRequestProperty(\"Accept\", \"application/json\")"
        "            if (acceptGzip) setRequestProperty(\"Accept-Encoding\", \"gzip\")"
-       "            if (contentType != null) setRequestProperty(\"Content-Type\", contentType)"
-       "            if (outputBytes != null) {"
-       "                require(byteCountWithinLimit(outputBytes, MAX_ZIP_TOTAL_BYTES)) { \"Upload exceeds byte limit\" }"
+       "            if (nullableTextPresent(contentType)) setRequestProperty(\"Content-Type\", nullableTextOrEmpty(contentType))"
+       "            if (nullableByteCountPresent(outputBytes)) {"
+       "                require(nullableByteCountWithinLimit(outputBytes, MAX_ZIP_TOTAL_BYTES)) { \"Upload exceeds byte limit\" }"
        "                doOutput = true"
-       "                setFixedLengthStreamingMode(outputBytes)"
+       "                setFixedLengthStreamingMode(nullableByteCountOrZero(outputBytes))"
        "            }"
        "        }"
        "        connection.connect()"
@@ -8670,13 +8689,14 @@
        "            val response = readResponseText(connection)"
        "            val payload = jsonObjectFromText(response)"
        "            val compactTruth = remoteTruthPayloadTruth(payload)"
-       "            if (compactTruth != null) {"
+       "            if (remoteTruthPayloadHasTruth(compactTruth)) {"
+       "                val remoteTruth = remoteTruthPayloadTruthOrEmpty(compactTruth)"
        "                val modified = remoteTruthPayloadMatchModified(payload)"
        "                var imported = 0"
-       "                if (writeRemoteTruth(compactTruth, modified)) imported += 1"
-       "                if (sameShipTruth(session, compactTruth)) {"
-       "                    val cached = truthForSession(session, compactTruth)"
-       "                    if (writeRemoteTruth(cached, truthTime(compactTruth) ?: modified, replaceIfMoreComplete = true)) {"
+       "                if (writeRemoteTruth(remoteTruth, modified)) imported += 1"
+       "                if (sameShipTruth(session, remoteTruth)) {"
+       "                    val cached = truthForSession(session, remoteTruth)"
+       "                    if (writeRemoteTruth(cached, truthTime(remoteTruth) ?: modified, replaceIfMoreComplete = true)) {"
        "                        imported += 1"
        "                    }"
        "                }"