package me.eternal.purrfect.nativelib import android.annotation.SuppressLint import android.content.Context import android.util.Log import java.io.File import kotlin.math.absoluteValue import kotlin.random.Random class NativeLib { var nativeUnaryCallCallback: (NativeRequestData) -> Unit = {} var signatureCache: String? = null companion object { var initialized = false private set private var libraryLoaded = false private fun findNativeLibraryDirs(): List { val app = runCatching { val cls = Class.forName("android.app.ActivityThread") val method = cls.getMethod("currentApplication") method.invoke(null) as? android.app.Application }.getOrNull() ?: return emptyList() val dirs = mutableListOf() val moduleDir = runCatching { app.createPackageContext(BuildConfig.MODULE_PACKAGE_NAME, Context.CONTEXT_IGNORE_SECURITY) .applicationInfo.nativeLibraryDir }.getOrNull() moduleDir?.let { dirs.add(File(it)) } app.applicationInfo.nativeLibraryDir?.let { dirs.add(File(it)) } return dirs.distinctBy { it.absolutePath } } private fun tryLoadFromNativeDir(): Boolean { val dirs = findNativeLibraryDirs().filter { it.isDirectory } if (dirs.isEmpty()) return false for (dir in dirs) { val candidates = listOf( "lib${BuildConfig.NATIVE_NAME}.so", "libpurrfect.so" ).map { File(dir, it) } val fallback = dir.listFiles() ?.firstOrNull { it.name.startsWith("lib") && it.name.endsWith(".so") && it.name.contains("purrfect") } val ordered = buildList { addAll(candidates) fallback?.let { if (!contains(it)) add(it) } } for (file in ordered) { if (!file.exists()) continue val ok = runCatching { System.load(file.absolutePath) libraryLoaded = true Log.i("Purrfect", "Fallbacked to ${file.absolutePath} via directory scan, loading it now.") true }.getOrDefault(false) if (ok) return true } } return false } fun tryEnsureLibraryLoaded(): Boolean { if (libraryLoaded) return true val candidates = linkedSetOf( BuildConfig.NATIVE_NAME, // Some repackagers restore the original Cargo output name. "purrfect", ).filter { it.isNotBlank() } var lastError: Throwable? = null for (name in candidates) { val ok = runCatching { if (name == "libpenguin") { Log.d("Purrfect", "Legacy load (libpenguin) skipped; searching for dynamic hash...") return@runCatching false } System.loadLibrary(name) libraryLoaded = true true }.onFailure { lastError = it }.getOrDefault(false) if (ok) return true } if (tryLoadFromNativeDir()) return true Log.e( "Purrfect", "Failed to load native library (tried: ${candidates.joinToString()})", lastError ) return false } fun ensureLibraryLoaded() { if (!tryEnsureLibraryLoaded()) { throw UnsatisfiedLinkError("Failed to load native library: ${BuildConfig.NATIVE_NAME}") } } } fun initOnce(callback: NativeLib.() -> Unit): () -> Unit { if (initialized) throw IllegalStateException("NativeLib already initialized") return runCatching { ensureLibraryLoaded() initialized = true callback(this) preInit() setChecksums(com.google.gson.Gson().toJson(Checksums.checksums)) return@runCatching { signatureCache = init(signatureCache) ?: run { Log.e("Purrfect", "Native initialization failed (Signature mismatch).") throw IllegalStateException("NativeLib init failed. Check logcat for more info") } } }.onFailure { initialized = false Log.e("Purrfect", "NativeLib init failed", it) }.getOrThrow() } @Suppress("unused") private fun onNativeUnaryCall(uri: String, buffer: ByteArray): NativeRequestData? { val nativeRequestData = NativeRequestData(uri, buffer) runCatching { nativeUnaryCallCallback(nativeRequestData) }.onFailure { Log.e("Purrfect", "nativeUnaryCallCallback failed", it) } if (nativeRequestData.canceled || !nativeRequestData.buffer.contentEquals(buffer)) return nativeRequestData return null } fun loadNativeConfig(config: NativeConfig) { if (!initialized) return loadConfig(config) } fun lockNativeDatabase(name: String, callback: () -> Unit) { if (!initialized) return lockDatabase(name) { runCatching { callback() }.onFailure { Log.e("Purrfect", "lockNativeDatabase callback failed", it) } } } @SuppressLint("UnsafeDynamicallyLoadedCode") fun loadSharedLibrary(content: ByteArray) { if (!initialized) throw IllegalStateException("NativeLib not initialized") val generatedPath = "/data/app/${Random.nextLong().absoluteValue.toString(16)}.so" addLinkerSharedLibrary(generatedPath, content) System.load(generatedPath) } private external fun preInit() private external fun init(signatureCache: String?): String? private external fun loadConfig(config: NativeConfig) private external fun lockDatabase(name: String, callback: Runnable) external fun setValdiLoader(code: String) private external fun addLinkerSharedLibrary(path: String, content: ByteArray) private external fun evaluateEndpointNative(uri: String, arg0: String, hasAttestation: Boolean, outDecision: NativeDecision) private external fun evaluateNetworkRequestNative(url: String, outDecision: NativeDecision) external fun shouldBlockDuplexClient(path: String): Boolean private external fun evaluateAuthContextNative(requestPath: String, attestationRequired: Boolean, outDecision: NativeDecision) private external fun evaluateApiInvocationNative(methodId: String, annotations: String, outDecision: NativeDecision) private external fun runEndpointSelfTest(testMode: Boolean): Boolean private external fun setChecksums(checksums: String) external fun setTestMode(testMode: Boolean) external fun setInLoginSignup(inLoginSignup: Boolean) fun evaluateEndpoint(uri: String, arg0: String, hasAttestation: Boolean): NativeDecision { return NativeDecision().also { evaluateEndpointNative(uri, arg0, hasAttestation, it) } } fun evaluateNetworkRequest(url: String): NativeDecision { return NativeDecision().also { evaluateNetworkRequestNative(url, it) } } fun evaluateAuthContext(requestPath: String, attestationRequired: Boolean): NativeDecision { return NativeDecision().also { evaluateAuthContextNative(requestPath, attestationRequired, it) } } fun evaluateApiInvocation(methodId: String, annotations: String): NativeDecision { return NativeDecision().also { evaluateApiInvocationNative(methodId, annotations, it) } } fun isEndpointBlockerHealthy(testMode: Boolean = false): Boolean { if (!initialized) return false return runEndpointSelfTest(testMode) } }