Generate SSD atomic writes from typed Jerboa
ober
250a3a58975294b5fdc0daff43db37677bb1002b
--- 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))" --- 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)'