MediaOwnershipTests.kt (4638B)
1 package tera.smoke 2 3 import com.sun.jna.Library 4 import com.sun.jna.Native 5 import java.nio.file.Files 6 import java.nio.file.Path 7 import kotlin.test.Test 8 import kotlin.test.assertEquals 9 import kotlin.test.assertFailsWith 10 import kotlin.test.assertNotNull 11 import kotlin.test.assertTrue 12 import kotlinx.coroutines.runBlocking 13 import uniffi.tera_core.FfiConverterTypeFfiMediaFile 14 import uniffi.tera_core.FfiMediaFile 15 import uniffi.tera_core.TeraAppException 16 17 internal interface SmokeLibC : Library { 18 fun open(path: String, flags: Int): Int 19 fun close(descriptor: Int): Int 20 fun dup2(source: Int, target: Int): Int 21 fun read(descriptor: Int, bytes: ByteArray, size: Long): Long 22 } 23 24 internal val LIBC = Native.load("c", SmokeLibC::class.java) 25 26 internal class SmokeFile(path: Path) : AutoCloseable { 27 val descriptor: Int = LIBC.open(path.toString(), 0).also { check(it >= 0) } 28 private var open = true 29 30 override fun close() { 31 if (open) { 32 open = false 33 check(LIBC.close(descriptor) == 0) 34 } 35 } 36 } 37 38 class MediaOwnershipTests { 39 @Test 40 fun callerCloseBeforeGeneratedConversionPreservesTheAdmittedBytes(): Unit = runBlocking { 41 SmokeFixture().use { fixture -> 42 val runtime = fixture.runtime() 43 try { 44 SmokeFile(fixture.original).use { original -> 45 FfiMediaFile(original.descriptor.toULong(), fixture.bytes.size.toULong()).use { file -> 46 original.close() 47 val saved = runtime.phase1SaveDraft( 48 "31".repeat(16), fixture.draft(listOf(fixture.media(file))), UNIX_S, null, UNIX_MS, 49 ) 50 assertEquals(fixture.sha256, assertNotNull(saved.form).media.single().sha256) 51 } 52 } 53 } finally { 54 runtime.shutdown() 55 runtime.close() 56 } 57 } 58 } 59 60 @Test 61 fun recycledCallerSlotAndDisposedSourceWrapperCannotSubstituteBytes(): Unit = runBlocking { 62 SmokeFixture().use { fixture -> 63 val runtime = fixture.runtime() 64 val replacementPath = fixture.root.resolve("replacement.png") 65 Files.write(replacementPath, ByteArray(fixture.bytes.size)) 66 try { 67 SmokeFile(fixture.original).use { original -> 68 SmokeFile(replacementPath).use { replacement -> 69 val source = FfiMediaFile(original.descriptor.toULong(), fixture.bytes.size.toULong()) 70 // Clone while the source wrapper is live. Raw generated pointer 71 // converters must never be called on an already disposed wrapper. 72 val retained = FfiConverterTypeFfiMediaFile.lift(FfiConverterTypeFfiMediaFile.lower(source)) 73 source.close() 74 source.close() 75 retained.use { file -> 76 assertEquals(original.descriptor, LIBC.dup2(replacement.descriptor, original.descriptor)) 77 val substituted = ByteArray(fixture.bytes.size) 78 assertEquals(substituted.size.toLong(), LIBC.read(original.descriptor, substituted, substituted.size.toLong())) 79 assertTrue(substituted.all { it == 0.toByte() }) 80 val saved = runtime.phase1SaveDraft( 81 "32".repeat(16), fixture.draft(listOf(fixture.media(file))), UNIX_S, null, UNIX_MS, 82 ) 83 assertEquals(fixture.sha256, assertNotNull(saved.form).media.single().sha256) 84 } 85 } 86 } 87 } finally { 88 runtime.shutdown() 89 runtime.close() 90 } 91 } 92 } 93 94 @Test 95 fun admissionRejectsInvalidDescriptorsAndWrongTypesSynchronously() { 96 assertFailsWith<TeraAppException.Failure> { FfiMediaFile(ULong.MAX_VALUE, 24uL) } 97 SmokeFixture().use { fixture -> 98 SmokeFile(fixture.root).use { directory -> 99 assertFailsWith<TeraAppException.Failure> { FfiMediaFile(directory.descriptor.toULong(), 24uL) } 100 } 101 SmokeFile(fixture.original).use { original -> 102 val failure = assertFailsWith<TeraAppException.Failure> { 103 FfiMediaFile(original.descriptor.toULong(), fixture.bytes.size.toULong() + 1uL) 104 } 105 assertEquals("media_size_mismatch", failure.report.code) 106 } 107 } 108 } 109 }