Generate SSD atomic writes from typed Jerboa

ober

250a3a58975294b5fdc0daff43db37677bb1002b

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 40b2c5b..6ebdacc 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -15566,9 +15566,12 @@
                       (java io InputStream)
                       (java io OutputStream)
                       (java net URL)
+                      (java nio channels FileChannel)
                       (java nio file Files)
                       (java nio file LinkOption)
                       (java nio file Path)
+                      (java nio file StandardCopyOption)
+                      (java nio file StandardOpenOption)
                       (java security MessageDigest)
                       (java time Instant)
                       (java time OffsetDateTime)
@@ -15588,6 +15591,7 @@
                 truthStoreStorageSummaryLocal
                 truthStoreStorageStatsLocal
                 truthStoreEnsureStorageCapacityLocal
+                truthStoreAtomicWriteLocal
                 truthStoreLoadRemoteConfigLocal
                 truthStoreRemoteUrlLocal
                 truthStoreInvalidateTruthIndexLocal
@@ -15632,6 +15636,7 @@
         (type DocumentFile)
         (type Exception)
         (type File)
+        (type FileChannel)
         (type Instant)
         (type InputStream)
         (type Int32)
@@ -15647,6 +15652,8 @@
         (type SsdSession)
         (type SharedPreferences)
         (type StorageStats)
+        (type StandardCopyOption)
+        (type StandardOpenOption)
         (type TruthIndexEntry)
         (type TruthStore)
         (type Uri)
@@ -15899,6 +15906,9 @@
         (extern (truthStoreListFiles
                   (file : File)) : (Nullable (Array File))
           (kotlin-member-call listFiles))
+        (extern (truthStoreFileParentFile
+                  (file : File)) : (Nullable File)
+          (kotlin-member-get parentFile))
         (extern (truthStoreFileInputStream
                   (file : File)) : InputStream
           (kotlin-member-call inputStream))
@@ -15986,6 +15996,37 @@
                   (directory : Path)
                   (prefix : String)) : Path
           (kotlin-call Files createTempDirectory))
+        (extern (truthStoreFilesCreateTempFile
+                  (directory : Path)
+                  (prefix : String)
+                  (suffix : String)) : Path
+          (kotlin-call Files createTempFile))
+        (extern (truthStoreFilesWriteWithOptions
+                  (path : Path)
+                  (bytes : Bytes)
+                  (truncateExisting : StandardOpenOption)
+                  (write : StandardOpenOption)) : Path
+          (kotlin-call Files write))
+        (extern (truthStoreFileChannelOpenWrite
+                  (path : Path)
+                  (write : StandardOpenOption)) : FileChannel
+          (kotlin-call FileChannel open))
+        (extern (truthStoreFileChannelForce
+                  (channel : FileChannel)
+                  (metadata : Bool)) : Unit
+          (kotlin-member-call force))
+        (extern (truthStoreFileChannelClose
+                  (channel : FileChannel)) : Unit
+          (kotlin-member-call close))
+        (extern (truthStoreFilesMoveAtomicReplace
+                  (source : Path)
+                  (target : Path)
+                  (atomicMove : StandardCopyOption)
+                  (replaceExisting : StandardCopyOption)) : Path
+          (kotlin-call Files move))
+        (extern (truthStoreFilesDeleteIfExists
+                  (path : Path)) : Bool
+          (kotlin-call Files deleteIfExists))
         (extern (truthStoreCreateTempFile
                   (prefix : String)
                   (suffix : String)
@@ -15999,6 +16040,14 @@
           (kotlin-member-call deleteRecursively))
         (extern (truthStoreNoFollowLinks) : LinkOption
           (kotlin-value LinkOption NOFOLLOW_LINKS))
+        (extern (truthStoreStandardOpenTruncateExisting) : StandardOpenOption
+          (kotlin-value StandardOpenOption TRUNCATE_EXISTING))
+        (extern (truthStoreStandardOpenWrite) : StandardOpenOption
+          (kotlin-value StandardOpenOption WRITE))
+        (extern (truthStoreStandardCopyAtomicMove) : StandardCopyOption
+          (kotlin-value StandardCopyOption ATOMIC_MOVE))
+        (extern (truthStoreStandardCopyReplaceExisting) : StandardCopyOption
+          (kotlin-value StandardCopyOption REPLACE_EXISTING))
         (extern (truthStoreIllegalArgumentException
                   (message : String)) : Exception
           (kotlin-call IllegalArgumentException))
@@ -16279,6 +16328,89 @@
             (throw
               (truthStoreIllegalArgumentException message)
               Unit)))
+        (def (truthStoreDestinationParentLocal
+               (destination : File)) : File
+          (let ((parent (truthStoreFileParentFile destination)))
+            (if (nullable-null? parent)
+              (throw
+                (truthStoreIllegalArgumentException
+                  "Destination has no parent")
+                File)
+              (nullable-get parent))))
+        (def (truthStoreFilesDeleteIfExistsUnit
+               (path : Path)) : Unit
+          (begin
+            (truthStoreFilesDeleteIfExists path)
+            (begin)))
+        (def (truthStoreForceFileChannelLocal
+               (path : Path)) : Unit
+          (let ((channel
+                  (truthStoreFileChannelOpenWrite
+                    path
+                    (truthStoreStandardOpenWrite))))
+            (try-finally
+              (truthStoreFileChannelForce channel #t)
+              (truthStoreFileChannelClose channel))))
+        (def (truthStoreAtomicMoveLocal
+               (temporary : Path)
+               (target : Path)
+               (bytes : Bytes)) : Unit
+          (begin
+            (truthStoreFilesWriteWithOptions
+              temporary
+              bytes
+              (truthStoreStandardOpenTruncateExisting)
+              (truthStoreStandardOpenWrite))
+            (truthStoreForceFileChannelLocal temporary)
+            (truthStoreFilesMoveAtomicReplace
+              temporary
+              target
+              (truthStoreStandardCopyAtomicMove)
+              (truthStoreStandardCopyReplaceExisting))
+            (begin)))
+        (def (truthStoreAtomicWriteLocal
+               (store : TruthStore)
+               (destination : File)
+               (bytes : Bytes)) : Unit
+          (begin
+            (truthStoreEnsureStorageCapacityLocal
+              store
+              destination
+              (byteArraySizeBytes bytes))
+            (let ((parent
+                    (truthStoreDestinationParentLocal destination)))
+              (let ((parentPath
+                      (truthStorePathToRealPath
+                        (truthStoreFileToPath parent)
+                        (truthStoreNoFollowLinks))))
+                (begin
+                  (truthStoreRequired
+                    (pathNotSymbolicLink parentPath)
+                    "Symlinked destination parent rejected")
+                  (let ((target
+                          (truthStorePathNormalize
+                            (truthStorePathResolve
+                              parentPath
+                              (truthStoreFileName destination)))))
+                    (begin
+                      (truthStoreRequired
+                        (pathParentMatches target parentPath)
+                        "Destination escaped parent")
+                      (truthStoreRequired
+                        (pathNotSymbolicLink target)
+                        "Symlinked destination rejected")
+                      (let ((temporary
+                              (truthStoreFilesCreateTempFile
+                                parentPath
+                                ".ssd-review-"
+                                ".part")))
+                        (try-finally
+                          (truthStoreAtomicMoveLocal
+                            temporary
+                            target
+                            bytes)
+                          (truthStoreFilesDeleteIfExistsUnit
+                            temporary))))))))))
         (def (truthStorePreferenceString
                (preferences : SharedPreferences)
                (key : String)) : String
@@ -18187,29 +18319,8 @@
        "    private fun ensureStorageCapacity(destination: File, bytes: Long) ="
        "        truthStoreEnsureStorageCapacityLocal(this, destination, bytes)"
        ""
-       "    internal fun atomicWrite(destination: File, bytes: ByteArray) {"
-       "        ensureStorageCapacity(destination, byteArraySizeBytes(bytes))"
-       "        if (!fileParentFilePresent(destination)) throw IllegalArgumentException(\"Destination has no parent\")"
-       "        val parent = checkNotNull(destination.parentFile)"
-       "        val parentPath = parent.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS)"
-       "        require(pathNotSymbolicLink(parentPath)) { \"Symlinked destination parent rejected\" }"
-       "        val target = parentPath.resolve(destination.name).normalize()"
-       "        require(pathParentMatches(target, parentPath)) { \"Destination escaped parent\" }"
-       "        require(pathNotSymbolicLink(target)) { \"Symlinked destination rejected\" }"
-       "        val temporary = Files.createTempFile(parentPath, \".ssd-review-\", \".part\")"
-       "        try {"
-       "            Files.write(temporary, bytes, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)"
-       "            FileChannel.open(temporary, StandardOpenOption.WRITE).use { it.force(true) }"
-       "            Files.move("
-       "                temporary,"
-       "                target,"
-       "                StandardCopyOption.ATOMIC_MOVE,"
-       "                StandardCopyOption.REPLACE_EXISTING"
-       "            )"
-       "        } finally {"
-       "            Files.deleteIfExists(temporary)"
-       "        }"
-       "    }"
+       "    internal fun atomicWrite(destination: File, bytes: ByteArray) ="
+       "        truthStoreAtomicWriteLocal(this, destination, bytes)"
        ""
        "    internal fun atomicWriteText(destination: File, text: String) ="
        "        atomicWrite(destination, textUtf8Bytes(text))"
diff --git a/tests/ssd-security-test.sh b/tests/ssd-security-test.sh
index 65b9c8b..f414d58 100755
--- a/tests/ssd-security-test.sh
+++ b/tests/ssd-security-test.sh
@@ -55,8 +55,8 @@ require_text 'Remote SSD service is not locally configured'
 require_generated_text 'validatedSourceKey'
 require_generated_text 'truthStoreSha256Hex'
 require_generated_text 'textUtf8Bytes(key)'
-require_text 'StandardCopyOption.ATOMIC_MOVE'
-require_text 'LinkOption.NOFOLLOW_LINKS'
+require_generated_text 'StandardCopyOption.ATOMIC_MOVE'
+require_generated_text 'LinkOption.NOFOLLOW_LINKS'
 require_generated_text 'LimitedInputStream(input, MAX_REMOTE_ZIP_BYTES)'
 require_generated_text 'LimitedInputStream(connection.inputStream, MAX_HTTP_COMPRESSED_BYTES)'
 require_generated_text 'MAX_HTTP_COMPRESSED_BYTES: Long = ((2L * 1024L) * 1024L)'