diff --git a/pdfium/build.gradle.kts b/pdfium/build.gradle.kts index aef24bb..26f9993 100644 --- a/pdfium/build.gradle.kts +++ b/pdfium/build.gradle.kts @@ -152,6 +152,7 @@ kotlin { // kotlinx-browser exposes the org.khronos.webgl.* typed arrays as a shared API // across js + wasmJs, so they resolve in the webMain metadata compilation. webMain.dependencies { implementation(libs.kotlinx.browser) } + jvmTest.dependencies { implementation(libs.kotlin.test) } } } diff --git a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt index 1e5e3e4..8530bd3 100644 --- a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt +++ b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt @@ -199,8 +199,9 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?) for (i in 0 until POOL_SIZE) { val h = PdfiumBridge.nOpenDocumentFromMemory(bufferAddr, bytes.size.toLong(), password) if (h == 0L) { + // NOTE: the buffer is freed by the catch below — freeing it here too + // double-frees it (native abort, not an exception). for (j in 0 until i) PdfiumBridge.nCloseDocument(handles[j]) - PdfiumBridge.nFreeBuffer(bufferAddr) error("PDFium refused to open document (err=${PdfiumBridge.nGetLastError()})") } handles[i] = h diff --git a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt index d17425f..e570657 100644 --- a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt +++ b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt @@ -224,9 +224,9 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?) for (i in 0 until POOL_SIZE) { val h = PdfiumBridge.nOpenDocumentFromMemory(bufferAddr, bytes.size.toLong(), password) if (h == 0L) { - // Cleanup previously opened handles + buffer on failure. + // Cleanup previously opened handles. The buffer is freed by the catch + // below — freeing it here too double-frees it (native abort). for (j in 0 until i) PdfiumBridge.nCloseDocument(handles[j]) - PdfiumBridge.nFreeBuffer(bufferAddr) error("PDFium refused to open document (err=${PdfiumBridge.nGetLastError()})") } handles[i] = h diff --git a/pdfium/src/jvmTest/kotlin/dev/nucleusframework/pdfium/OpenPdfDocumentTest.kt b/pdfium/src/jvmTest/kotlin/dev/nucleusframework/pdfium/OpenPdfDocumentTest.kt new file mode 100644 index 0000000..c9f40b6 --- /dev/null +++ b/pdfium/src/jvmTest/kotlin/dev/nucleusframework/pdfium/OpenPdfDocumentTest.kt @@ -0,0 +1,52 @@ +package dev.nucleusframework.pdfium + +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlinx.coroutines.runBlocking + +class OpenPdfDocumentTest { + /** + * Regression: bytes PDFium refuses must surface as an exception, not kill the + * process. The open-failure branch used to free the native buffer that the + * enclosing `catch` frees as well, so a non-PDF body double-freed it and the + * host aborted — SIGABRT on the JVM, "Scudo ERROR: invalid chunk state when + * deallocating" on Android. Reaching the assertion at all IS the test. + */ + @Test + fun refusedBytesThrowInsteadOfAbortingTheProcess() = runBlocking { + assertFailsWith { + openPdfDocument("definitely not a pdf".encodeToByteArray(), null) + } + Unit + } + + @Test + fun aValidDocumentStillOpens() = runBlocking { + val doc = openPdfDocument(minimalPdf(), null) + try { + kotlin.test.assertEquals(1, doc.pageCount) + } finally { + doc.close() + } + } +} + +/** A minimal, valid one-page PDF (built-in /Helvetica, no embedded font program). */ +private fun minimalPdf(): ByteArray = java.util.Base64.getDecoder().decode( + "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2Jq" + + "CjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2Jq" + + "CjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIg" + + "NzkyXSAvQ29udGVudHMgNCAwIFIgL1Jlc291cmNlcyA8PCAvRm9udCA8PCAvRjEgNSAwIFIgPj4g" + + "Pj4gPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCAzMzEgPj4Kc3RyZWFtCkJUIC9GMSAyNCBU" + + "ZiA3MiA3MDAgVGQgMjggVEwgKFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6" + + "eSBkb2cuKSBUaiBUKiAoSW52b2ljZSAjMjAyNi0wNzE0ICBBbW91bnQgZHVlOiAkMSwyMzQuNTYp" + + "IFRqIFQqIChUaGlzIGlzIGEgdGV4dC1vbmx5IFBERiB0byBleGVyY2lzZSBwZGZpdW0gYXN5bmMg" + + "Z2x5cGggcGFpbnQuKSBUaiBUKiAoTGluZSBmb3VyIHdpdGggbW9yZSB3b3JkcyB0byBmaWxsIHRo" + + "ZSBwYWdlIGJvZHkgYXJlYSBuaWNlbHkuKSBUaiBUKiAoQ29udGFjdDogY2xhdWRlQG1pa2VwZW56" + + "LmRldiAgIFJlZjogUFJFVklFVy1SRVBSTykgVGogVCogRVQKZW5kc3RyZWFtCmVuZG9iago1IDAg" + + "b2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+" + + "PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4g" + + "CjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAw" + + "IG4gCjAwMDAwMDA2MjMgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+" + + "CnN0YXJ0eHJlZgo2OTMKJSVFT0Y=" +)