diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt index b9ded183..dbbc7f3d 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt @@ -232,6 +232,7 @@ class FFMpegProcessor( } globalArguments += "-ar" to args.audioStreamFormat.sampleRate.toString() globalArguments += "-ac" to args.audioStreamFormat.channels.toString() + outputArguments += "-c:a" to "pcm_s16le" } Action.MERGE_AUDIO_STREAMS -> { inputArguments.clear() @@ -240,18 +241,23 @@ class FFMpegProcessor( args.inputs.forEachIndexed { index, input -> inputArguments += "-i" to input val offset = args.inputDelayOffsets?.get(input) ?: 0L + filterParts.append("[$index:a]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo") if (offset > 0) { - filterParts.append("[$index:a]adelay=$offset|$offset[a$index];") + filterParts.append(",adelay=$offset|$offset[a$index];") } else { - filterParts.append("[$index:a]acopy[a$index];") + filterParts.append(",acopy[a$index];") } } args.inputs.indices.forEach { index -> filterParts.append("[a$index]") } - filterParts.append("amix=inputs=${args.inputs.size}:duration=longest:normalize=0[aout]") + filterParts.append("amix=inputs=${args.inputs.size}:duration=longest:dropout_transition=0:normalize=1,alimiter=limit=0.95[aout]") outputArguments += "-filter_complex" to "\"$filterParts\"" outputArguments += "-map" to "\"[aout]\"" + outputArguments += "-c:a" to "libmp3lame" + outputArguments += "-b:a" to "192k" + outputArguments += "-ar" to "48000" + outputArguments += "-ac" to "2" } } outputArguments += args.output.absolutePath diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/download/call/CallDownloadSessionImpl.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/download/call/CallDownloadSessionImpl.kt index f437a875..70101e97 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/call/CallDownloadSessionImpl.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/call/CallDownloadSessionImpl.kt @@ -46,7 +46,7 @@ class CallDownloadSessionImpl( val job: Job val writePfd: ParcelFileDescriptor - val outputFile = context.androidContext.cacheDir.resolve("call_${UUID.randomUUID()}.mp3").apply { + val outputFile = context.androidContext.cacheDir.resolve("call_${UUID.randomUUID()}.wav").apply { if (exists()) delete() } diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index e7294424..79c5e5e1 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -537,7 +537,8 @@ }, "file_imports": { "no_files_settings_hint": "No files found. Make sure you have imported the required files in the File Imports section", - "settings_select_file_hint": "Select an imported file" + "settings_select_file_hint": "Select an imported file", + "settings_select_file_subtitle": "Choose a file from your imported files list" } }, "scripting": { @@ -2910,6 +2911,9 @@ "text_field_selection": "{amount} selected", "text_field_selection_all": "All", "export_file_format_title": "Export File Format", + "sort_order_title": "Message Order", + "sort_order_newest_to_oldest": "Newest to Oldest", + "sort_order_oldest_to_newest": "Oldest to Newest", "message_type_filter_title": "Filter Messages by Type", "amount_of_messages_title": "Message Count (leave blank for all)", "download_medias_title": "Download Media" diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ExportChatMessages.kt index 21b87a71..f1a9ad08 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ExportChatMessages.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ExportChatMessages.kt @@ -74,6 +74,7 @@ import me.eternal.purrfectsnap.core.logger.CoreLogger import me.eternal.purrfectsnap.core.messaging.ConversationExporter import me.eternal.purrfectsnap.core.messaging.ExportFormat import me.eternal.purrfectsnap.core.messaging.ExportParams +import me.eternal.purrfectsnap.core.messaging.ExportSortOrder import me.eternal.purrfectsnap.core.wrapper.impl.Message import java.io.File import kotlin.math.absoluteValue @@ -180,7 +181,9 @@ class ExportChatMessages : AbstractAction() { var downloadMedias by remember { mutableStateOf(false) } var showConversationPicker by remember { mutableStateOf(false) } var showFormatPicker by remember { mutableStateOf(false) } + var showOrderPicker by remember { mutableStateOf(false) } var showMessageTypePicker by remember { mutableStateOf(false) } + var exportSortOrder by remember { mutableStateOf(ExportSortOrder.NEWEST_TO_OLDEST) } val colorOverrides = remember { mutableStateMapOf() } var colorPickerTarget by remember { mutableStateOf(null) } var colorPickerValue by remember { mutableStateOf(null) } @@ -309,6 +312,19 @@ class ExportChatMessages : AbstractAction() { tint = MaterialTheme.colorScheme.surfaceVariant ) + SectionLabel(t("sort_order_title")) + GlassField( + value = t( + if (exportSortOrder == ExportSortOrder.OLDEST_TO_NEWEST) { + "sort_order_oldest_to_newest" + } else { + "sort_order_newest_to_oldest" + } + ), + onClick = { showOrderPicker = true }, + tint = MaterialTheme.colorScheme.surfaceVariant + ) + SectionLabel(t("message_type_filter_title")) GlassField( value = messageTypeFilter.takeIf { it.isNotEmpty() }?.let { @@ -417,6 +433,7 @@ class ExportChatMessages : AbstractAction() { selection, ExportParams( exportFormat = exportType, + sortOrder = exportSortOrder, messageTypeFilter = messageTypeFilter.takeIf { it.isNotEmpty() }, amountOfMessages = amountOfMessages.takeIf { it != -1 }, downloadMedias = downloadMedias, @@ -610,6 +627,38 @@ class ExportChatMessages : AbstractAction() { ) } } + + if (showOrderPicker) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.55f)) + .clickable { showOrderPicker = false } + ) + SelectorPopup( + title = t("sort_order_title"), + onDismiss = { showOrderPicker = false } + ) { + SelectorRow( + title = t("sort_order_newest_to_oldest"), + subtitle = null, + checked = exportSortOrder == ExportSortOrder.NEWEST_TO_OLDEST, + onToggle = { + exportSortOrder = ExportSortOrder.NEWEST_TO_OLDEST + showOrderPicker = false + } + ) + SelectorRow( + title = t("sort_order_oldest_to_newest"), + subtitle = null, + checked = exportSortOrder == ExportSortOrder.OLDEST_TO_NEWEST, + onToggle = { + exportSortOrder = ExportSortOrder.OLDEST_TO_NEWEST + showOrderPicker = false + } + ) + } + } } } @@ -1159,6 +1208,13 @@ class ExportChatMessages : AbstractAction() { }.getOrDefault(emptyList()) } + private fun messageSortKey(message: Message): Long { + return message.orderKey + ?: message.messageMetadata?.createdAt + ?: message.messageDescriptor?.messageId + ?: Long.MIN_VALUE + } + private suspend fun exportFullConversation( feedEntry: FriendFeedEntry, exportParams: ExportParams, @@ -1209,58 +1265,75 @@ class ExportChatMessages : AbstractAction() { var foundMessageCount = 0 val exportedOrderKeys = mutableSetOf() + val fetchedMessages = mutableListOf() + val seenMessageKeys = mutableSetOf() var lastMessageId: Long? = null fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).firstOrNull()?.also { message -> - conversationExporter.readMessage(message) - foundMessageCount++ - message.orderKey?.let { exportedOrderKeys.add(it) } + val messageKey = message.orderKey?.toString() ?: message.messageDescriptor?.messageId?.toString() + if (messageKey != null && seenMessageKeys.add(messageKey)) { + fetchedMessages.add(message) + } lastMessageId = message.messageDescriptor?.messageId } - if (lastMessageId == null) { + if (lastMessageId == null && fetchedMessages.isEmpty()) { logDialog(translation["no_messages_found"]) } while (lastMessageId != null) { - val fetchedMessages = fetchMessagesPaginated(conversationId, lastMessageId, amount = 500).toMutableList() - if (fetchedMessages.isEmpty()) break + val pagedMessages = fetchMessagesPaginated(conversationId, lastMessageId, amount = 500) + if (pagedMessages.isEmpty()) break - fetchedMessages.firstOrNull()?.let { + pagedMessages.firstOrNull()?.let { lastMessageId = it.messageDescriptor!!.messageId!! } - exportParams.messageTypeFilter?.let { filter -> - fetchedMessages.removeIf { message -> - !filter.contains(message.messageContent?.contentType ?: return@removeIf false) + pagedMessages.forEach { message -> + val messageKey = message.orderKey?.toString() ?: message.messageDescriptor?.messageId?.toString() + if (messageKey != null && seenMessageKeys.add(messageKey)) { + fetchedMessages.add(message) } } + } - val remainingLimit = exportParams.amountOfMessages?.let { it - foundMessageCount } ?: Int.MAX_VALUE - if (remainingLimit <= 0) break - - val messagesToWrite = fetchedMessages.reversed().let { messages -> - if (messages.size <= remainingLimit) messages else messages.subList(0, remainingLimit) + val filteredMessages = exportParams.messageTypeFilter?.let { filter -> + fetchedMessages.filter { message -> + val contentType = message.messageContent?.contentType ?: return@filter false + filter.contains(contentType) } + } ?: fetchedMessages - messagesToWrite.forEach { message -> - conversationExporter.readMessage(message) - foundMessageCount++ - message.orderKey?.let { exportedOrderKeys.add(it) } - } + val sortedMessages = when (exportParams.sortOrder) { + ExportSortOrder.OLDEST_TO_NEWEST -> filteredMessages.sortedBy { messageSortKey(it) } + ExportSortOrder.NEWEST_TO_OLDEST -> filteredMessages.sortedByDescending { messageSortKey(it) } + } + val messagesToWrite = exportParams.amountOfMessages?.let { limit -> + sortedMessages.take(limit) + } ?: sortedMessages + + messagesToWrite.forEach { message -> + conversationExporter.readMessage(message) + foundMessageCount++ + message.orderKey?.let { exportedOrderKeys.add(it) } setStatus("Exporting (found ${foundMessageCount})") } if (loggerMessages.isNotEmpty() && (exportParams.amountOfMessages == null || foundMessageCount < exportParams.amountOfMessages)) { val parsedLoggerMessages = loggerMessages.mapNotNull { conversationExporter.parseLoggedMessage(it) } - for (loggedMessage in parsedLoggerMessages.asReversed()) { + val sortedLoggerMessages = when (exportParams.sortOrder) { + ExportSortOrder.OLDEST_TO_NEWEST -> parsedLoggerMessages.sortedBy { it.orderKey } + ExportSortOrder.NEWEST_TO_OLDEST -> parsedLoggerMessages.sortedByDescending { it.orderKey } + } + for (loggedMessage in sortedLoggerMessages) { if (exportedOrderKeys.contains(loggedMessage.orderKey)) continue val filter = exportParams.messageTypeFilter if (filter != null && !filter.contains(loggedMessage.contentType)) continue if (exportParams.amountOfMessages != null && foundMessageCount >= exportParams.amountOfMessages) break conversationExporter.readLoggedMessage(loggedMessage) foundMessageCount++ + setStatus("Exporting (found ${foundMessageCount})") } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/CallRecorder.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/CallRecorder.kt index ff4276f3..14b92f78 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/CallRecorder.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/CallRecorder.kt @@ -184,6 +184,29 @@ class CallRecorder : Feature("Call Recorder") { onCallStarted(conversationId) } + private fun clampCopyRange(offset: Int, requestedLength: Int, maxLength: Int): Pair? { + if (requestedLength <= 0 || maxLength <= 0) return null + val safeOffset = offset.coerceAtLeast(0).coerceAtMost(maxLength) + val available = (maxLength - safeOffset).coerceAtLeast(0) + val safeLength = requestedLength.coerceAtMost(available) + if (safeLength <= 0) return null + return safeOffset to safeLength + } + + private fun copyAudioRecordByteBuffer(data: ByteBuffer, bytesRead: Int): ByteArray? { + if (bytesRead <= 0) return null + val currentPosition = data.position() + val start = (currentPosition - bytesRead).coerceAtLeast(0) + val end = currentPosition.coerceAtMost(data.limit()) + if (end <= start) return null + return ByteArray(end - start).also { out -> + val dup = data.duplicate() + dup.position(start) + dup.limit(end) + dup.get(out) + } + } + override fun init() { if (callRecorderConfig.callRecorder.getNullable() == null) return @@ -221,10 +244,18 @@ class CallRecorder : Feature("Call Recorder") { val wrapper = streams[param.thisObject().hashCode()] ?: return@hook val buffer = when (val data = param.arg(0)) { - is ByteBuffer -> ByteArray(result).also { val pos = data.position(); data.get(it); data.position(pos) } - is ByteArray -> data.copyOfRange(param.argNullable(1) ?: 0, (param.argNullable(1) ?: 0) + result) - is ShortArray -> ByteArray(result * 2).also { - ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, param.argNullable(1) ?: 0, result) + is ByteBuffer -> copyAudioRecordByteBuffer(data, result) ?: return@hook + is ByteArray -> { + val offset = param.argNullable(1) ?: 0 + val (safeOffset, safeLength) = clampCopyRange(offset, result, data.size) ?: return@hook + data.copyOfRange(safeOffset, safeOffset + safeLength) + } + is ShortArray -> { + val offset = param.argNullable(1) ?: 0 + val (safeOffset, safeLength) = clampCopyRange(offset, result, data.size) ?: return@hook + ByteArray(safeLength * 2).also { + ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, safeOffset, safeLength) + } } else -> return@hook } @@ -260,14 +291,31 @@ class CallRecorder : Feature("Call Recorder") { hook("write", HookStage.BEFORE) { param -> val wrapper = streams[param.thisObject().hashCode()] ?: return@hook val data = param.arg(0) - val size = if (param.args().size > 2) param.arg(2) else if (data is ByteArray) data.size else if (data is ShortArray) data.size else if (data is ByteBuffer) data.remaining() else 0 - if (size <= 0) return@hook val buffer = when (data) { - is ByteBuffer -> ByteArray(size).also { val pos = data.position(); data.get(it); data.position(pos) } - is ByteArray -> data.copyOfRange(param.argNullable(1) ?: 0, (param.argNullable(1) ?: 0) + size) - is ShortArray -> ByteArray(size * 2).also { - ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, param.argNullable(1) ?: 0, size) + is ByteBuffer -> { + val requestedSize = param.argNullable(1) ?: data.remaining() + val safeSize = requestedSize.coerceAtMost(data.remaining()).coerceAtLeast(0) + if (safeSize <= 0) return@hook + ByteArray(safeSize).also { + val pos = data.position() + data.get(it, 0, safeSize) + data.position(pos) + } + } + is ByteArray -> { + val offset = param.argNullable(1) ?: 0 + val requestedSize = param.argNullable(2) ?: data.size + val (safeOffset, safeLength) = clampCopyRange(offset, requestedSize, data.size) ?: return@hook + data.copyOfRange(safeOffset, safeOffset + safeLength) + } + is ShortArray -> { + val offset = param.argNullable(1) ?: 0 + val requestedSize = param.argNullable(2) ?: data.size + val (safeOffset, safeLength) = clampCopyRange(offset, requestedSize, data.size) ?: return@hook + ByteArray(safeLength * 2).also { + ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, safeOffset, safeLength) + } } else -> return@hook } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/CustomEmojiFont.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/CustomEmojiFont.kt index ffbf9b32..6e9911bb 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/CustomEmojiFont.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/CustomEmojiFont.kt @@ -1,52 +1,84 @@ package me.eternal.purrfectsnap.core.features.impl.experiments +import android.graphics.Typeface import me.eternal.purrfectsnap.common.bridge.FileHandleScope import me.eternal.purrfectsnap.core.ModContext import java.io.File import java.io.FileOutputStream - -private var cacheFontPath: String? = null -private var cacheFontName: String? = null +private var cachedFontPath: String? = null +private var cachedFontName: String? = null fun getCustomEmojiFontPath( context: ModContext ): String? { - val customFileName = context.config.experimental.nativeHooks.customEmojiFont.getNullable()?.takeIf { it.isNotBlank() } ?: return null - val safeFileName = customFileName.substringAfterLast("/") + val customFileName = context.config.experimental.nativeHooks.customEmojiFont.getNullable() + ?.trim() + ?.takeIf { it.isNotBlank() } ?: return null + val safeFileName = customFileName.substringAfterLast("/").substringAfterLast("\\") + if (safeFileName.isBlank()) return null + + fun clearCachedPath(cacheFile: File? = null): String? { + runCatching { cacheFile?.delete() } + cachedFontPath = null + cachedFontName = safeFileName + return null + } + + fun validateFontFile(file: File): Boolean { + return runCatching { + Typeface.createFromFile(file) + true + }.onFailure { + context.log.warn("Custom emoji font validation failed for ${file.absolutePath}: ${it.message}") + }.getOrDefault(false) + } + return runCatching { val handle = context.fileHandlerManager.getFileHandle( FileHandleScope.USER_IMPORT.key, customFileName - ) ?: return@runCatching null + ) ?: return@runCatching clearCachedPath() if (!handle.exists()) { - File(context.androidContext.filesDir, "emoji_fonts") - .resolve(safeFileName) - .takeIf { it.exists() } - ?.delete() - cacheFontPath = "" - cacheFontName = safeFileName - return@runCatching null + val oldCacheFile = File(context.androidContext.cacheDir, "emoji_fonts").resolve(safeFileName) + return@runCatching clearCachedPath(oldCacheFile) } - val persistentDir = File(context.androidContext.filesDir, "emoji_fonts").apply { + val persistentDir = File(context.androidContext.cacheDir, "emoji_fonts").apply { mkdirs() } val persistentFile = File(persistentDir, safeFileName) - handle.open(android.os.ParcelFileDescriptor.MODE_READ_ONLY)?.use { pfd -> - if (!persistentFile.exists() || pfd.statSize != persistentFile.length()) { - FileOutputStream(persistentFile).use { output -> + + val needsCopy = handle.open(android.os.ParcelFileDescriptor.MODE_READ_ONLY)?.use { pfd -> + val sourceSize = pfd.statSize + !persistentFile.exists() || (sourceSize > 0 && sourceSize != persistentFile.length()) + } ?: true + + if (needsCopy) { + val tempFile = File(persistentDir, "$safeFileName.tmp") + handle.open(android.os.ParcelFileDescriptor.MODE_READ_ONLY)?.use { pfd -> + FileOutputStream(tempFile).use { output -> android.os.ParcelFileDescriptor.AutoCloseInputStream(pfd).use { input -> input.copyTo(output) } } } + if (!tempFile.renameTo(persistentFile)) { + tempFile.copyTo(persistentFile, overwrite = true) + tempFile.delete() + } } - cacheFontName = safeFileName - cacheFontPath = persistentFile.absolutePath - cacheFontPath?.takeIf { it.isNotEmpty() } + + if (!validateFontFile(persistentFile)) { + return@runCatching clearCachedPath(persistentFile) + } + + cachedFontName = safeFileName + cachedFontPath = persistentFile.absolutePath + cachedFontPath?.takeIf { it.isNotEmpty() } }.onFailure { context.log.error("Failed to get custom emoji font", it) + clearCachedPath() }.getOrNull() } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/ExportParams.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/ExportParams.kt index d37c25dd..029ea708 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/ExportParams.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/ExportParams.kt @@ -2,8 +2,14 @@ package me.eternal.purrfectsnap.core.messaging import me.eternal.purrfectsnap.common.data.ContentType +enum class ExportSortOrder { + NEWEST_TO_OLDEST, + OLDEST_TO_NEWEST +} + class ExportParams( val exportFormat: ExportFormat = ExportFormat.HTML, + val sortOrder: ExportSortOrder = ExportSortOrder.NEWEST_TO_OLDEST, val messageTypeFilter: List? = null, val amountOfMessages: Int? = null, val downloadMedias: Boolean = false, diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt index 78cb9dff..575a9ca5 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt @@ -11,6 +11,16 @@ object LSPatchUpdater { var HAS_LSPATCH = false private set + private fun ensureTranslationsLoaded(context: ModContext) { + if (context.translation.getOrNull("toast_purrfectsnap_updated") != null) return + runCatching { + context.translation.userLocale = context.getConfigLocale() + context.translation.load() + }.onFailure { + context.log.warn("Failed to load translations in updater: ${it.message}", TAG) + } + } + private fun getModuleUniqueHash(module: ZipFile): String { return module.entries().asSequence() .filter { !it.isDirectory } @@ -20,6 +30,8 @@ object LSPatchUpdater { } fun onBridgeConnected(context: ModContext) { + ensureTranslationsLoaded(context) + val obfuscatedModulePath by lazy { (runCatching { context::class.java.classLoader?.loadClass("org.lsposed.lspatch.share.Constants") @@ -59,19 +71,19 @@ object LSPatchUpdater { } context.log.verbose("updating", TAG) - context.shortToast(context.translation["toast_updating_purrfectsnap"]) + context.shortToast(context.translation.getOrNull("toast_updating_purrfectsnap") ?: "Updating PurrfectSnap. Please wait...") // copy embedded module to cache runCatching { seAppApk.copyTo(embeddedModule, overwrite = true) }.onFailure { seAppApk.delete() context.log.error("Failed to copy embedded module", it, TAG) - context.longToast(context.translation["toast_update_purrfectsnap_failed"]) + context.longToast(context.translation.getOrNull("toast_update_purrfectsnap_failed") ?: "Failed to update PurrfectSnap. Please check logcat for more details.") context.forceCloseApp() return } - context.longToast(context.translation["toast_purrfectsnap_updated"]) + context.longToast(context.translation.getOrNull("toast_purrfectsnap_updated") ?: "PurrfectSnap updated!") context.log.verbose("updated", TAG) context.softRestartApp() } diff --git a/native/rust/src/modules/custom_font_hook.rs b/native/rust/src/modules/custom_font_hook.rs index d26cfb2d..c97ec6ee 100644 --- a/native/rust/src/modules/custom_font_hook.rs +++ b/native/rust/src/modules/custom_font_hook.rs @@ -1,4 +1,4 @@ -use std::{ffi::CStr, fs}; +use std::{ffi::{CStr, CString}, fs}; use nix::libc::{self, c_uint}; @@ -12,7 +12,18 @@ def_hook!( if pathname == "/system/fonts/NotoColorEmoji.ttf" { if let Some(font_path) = config::native_config().custom_emoji_font_path { if fs::metadata(&font_path).is_ok() { - return libc::openat(libc::AT_FDCWD, font_path.as_ptr() as *const u8, flags, mode); + match CString::new(font_path.clone()) { + Ok(c_font_path) => { + let fd = libc::openat(libc::AT_FDCWD, c_font_path.as_ptr() as *const u8, flags, mode); + if fd >= 0 { + return fd; + } + warn!("failed to open custom emoji font path (fd={}): {}", fd, font_path); + } + Err(_) => { + warn!("custom emoji font path contains null byte, using fallback system font"); + } + } } else { warn!("custom emoji font path does not exist: {}", font_path); } @@ -31,4 +42,4 @@ pub fn init() { } dobby_hook_sym!("libc.so", "open", open_hook); -} \ No newline at end of file +}