diff --git a/announcements.txt b/announcements.txt new file mode 100644 index 00000000..26b501fa --- /dev/null +++ b/announcements.txt @@ -0,0 +1,3 @@ +# PurrfectSnap Announcements + +- Welcome to PurrfectSnap announcements. \ No newline at end of file diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt index 8df45643..5956a5e7 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt @@ -149,6 +149,7 @@ class HomeRootSection : Routes.Route() { private val changelogClient by lazy { OkHttpClient() } private val changelogStableUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/changelogs-stable.txt" private val changelogPrereleaseUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/changelogs-prerelease.txt" + private val announcementsUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/announcements.txt" private val heroGradientColors = listOf( Color(0xFF5C4B99), @@ -748,6 +749,10 @@ class HomeRootSection : Routes.Route() { var changelogError by remember { mutableStateOf(null) } var changelogText by remember { mutableStateOf(null) } var changelogVersion by remember { mutableStateOf(null) } + var showAnnouncementsDialog by remember { mutableStateOf(false) } + var announcementsLoading by remember { mutableStateOf(false) } + var announcementsError by remember { mutableStateOf(null) } + var announcementsText by remember { mutableStateOf(null) } val handleUpdateAction: () -> Unit = { latestUpdate?.let { latest -> @@ -827,6 +832,31 @@ class HomeRootSection : Routes.Route() { } } + fun loadAnnouncements() { + if (announcementsText != null) return + announcementsLoading = true + announcementsError = null + coroutineScope.launch(Dispatchers.IO) { + runCatching { + changelogClient.newCall(Request.Builder().url(announcementsUrl).build()).execute().use { response -> + if (!response.isSuccessful) throw IllegalStateException("Failed to fetch announcements (${response.code})") + val body = response.body?.string() ?: throw IllegalStateException("Empty announcements body") + body.trim() + } + }.onSuccess { text -> + withContext(Dispatchers.Main) { + announcementsText = text + announcementsLoading = false + } + }.onFailure { error -> + withContext(Dispatchers.Main) { + announcementsError = error.message ?: "Failed to load announcements" + announcementsLoading = false + } + } + } + } + val onUpdateButtonClick: () -> Unit = { latestUpdate?.let { showChangelogDialog = true @@ -859,6 +889,13 @@ class HomeRootSection : Routes.Route() { horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { + TopBarActionChip( + icon = Icons.Filled.Info, + label = "Announcements" + ) { + showAnnouncementsDialog = true + loadAnnouncements() + } Spacer(modifier = Modifier.weight(1f)) HomeActionChips() } @@ -1143,6 +1180,68 @@ class HomeRootSection : Routes.Route() { ) } + if (showAnnouncementsDialog) { + AestheticDialog( + onDismissRequest = { showAnnouncementsDialog = false }, + title = "Announcements", + text = "", + icon = Icons.Filled.Info, + confirmButtonText = "Close", + onConfirm = { showAnnouncementsDialog = false }, + dismissButtonText = "Dismiss", + onDismiss = { showAnnouncementsDialog = false }, + confirmEnabled = !announcementsLoading, + customContent = { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 120.dp, max = 340.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + when { + announcementsLoading -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(28.dp), + strokeWidth = 3.dp, + color = Color.White + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = "Loading announcements...", + color = Color.White, + fontWeight = FontWeight.SemiBold + ) + } + } + + announcementsError != null -> { + Text( + text = announcementsError ?: "Failed to load announcements", + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.SemiBold + ) + } + + else -> { + Text( + text = announcementsText ?: "Announcements not available", + color = PurrfectPalette.textPrimary, + fontSize = 14.sp, + lineHeight = 20.sp + ) + } + } + } + } + ) + } + if (showQuickActionsMenu) { QuickActionsDialog( quickActions = cards, diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt index 1899dfe9..57530218 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt @@ -339,7 +339,7 @@ class PatchSnapchatScreen : SetupScreen() { textAlign = TextAlign.Start ) Text( - text = "Fix: Download and install JingMatrix LSPatch, then patch Snapchat 13.64.0.52 in Integrated mode. Select Embed Modules and embed the PurrfectSnap APK. Then choose Skip auto setup during PurrfectSnap setup to skip Auto Patcher.", + text = "Fix: Download and install JingMatrix LSPatch, then patch Snapchat 13.65.1.0 to 13.71.0.51 in Integrated mode. Select Embed Modules and embed the PurrfectSnap APK. Then choose Skip auto setup during PurrfectSnap setup to skip Auto Patcher.", style = bodyStyle, textAlign = TextAlign.Start ) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt index 6d07dd90..82422ca7 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt @@ -4,40 +4,36 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.People -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -48,30 +44,17 @@ import me.eternal.purrfectsnap.common.data.FriendLinkType import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog import me.eternal.purrfectsnap.core.action.AbstractAction import me.eternal.purrfectsnap.core.event.events.impl.ActivityResultEvent -import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie -import me.eternal.purrfectsnap.common.util.snap.RemoteMediaResolver import me.eternal.purrfectsnap.core.features.impl.experiments.AddFriendSourceSpoof import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging +import me.eternal.purrfectsnap.core.util.EvictingMap import me.eternal.purrfectsnap.core.wrapper.impl.Snapchatter +import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie +import me.eternal.purrfectsnap.common.util.snap.RemoteMediaResolver import kotlin.random.Random +import java.text.SimpleDateFormat +import java.util.* class ManageFriendList : AbstractAction() { - companion object { - private var openSuggestedOnLaunch = false - - @Synchronized - fun requestOpenSuggestedOnLaunch() { - openSuggestedOnLaunch = true - } - - @Synchronized - private fun consumeOpenSuggestedOnLaunch(): Boolean { - val shouldOpen = openSuggestedOnLaunch - openSuggestedOnLaunch = false - return shouldOpen - } - } - private val translation by lazy { context.translation.getCategory("friend_list") } private val dialogBackground = Brush.verticalGradient( listOf( @@ -92,211 +75,7 @@ class ManageFriendList : AbstractAction() { ) ) private var pendingPickerAction: Pair Unit>? = null - - private val uuidRegex by lazy { - Regex("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}") - } - - private fun loadStaticDefaultField(classLoader: ClassLoader, className: String, fieldName: String): Any? { - return try { - val clazz = classLoader.loadClass(className) - try { - val field = clazz.getDeclaredField(fieldName) - field.isAccessible = true - field.get(null) - } catch (e: Exception) { - clazz.declaredFields - .filter { java.lang.reflect.Modifier.isStatic(it.modifiers) } - .firstOrNull() - ?.apply { isAccessible = true } - ?.get(null) - } - } catch (e: Exception) { - context.log.warn("Could not load $className: ${e.message}") - null - } - } - - private fun addFriend(userId: String) { - val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance - if (friendRelationshipChangerInstance == null) { - context.log.error("friendRelationshipChangerInstance is null") - context.longToast("Failed to add friend: FriendRelationshipChanger instance not available") - return - } - - runCatching { - val classLoader = context.androidContext.classLoader - - val classNamesToTry = listOf("EnumC3559qC", "qC", "com.snapchat.android.EnumC3559qC", "LC", "EnumC0886LC", "EnumC1539TC", "TC") - - val enumC3559qCClass = classNamesToTry.firstNotNullOfOrNull { className -> - try { - classLoader.loadClass(className).takeIf { it.isEnum }?.also { - context.log.verbose("Successfully loaded enum class: $className") - } - } catch (e: Exception) { - null - } - } - - if (enumC3559qCClass == null) { - context.log.error("Friend source enum class not found after trying: ${classNamesToTry.joinToString()}") - context.longToast("Failed to add friend: Required enum class not found") - return@runCatching - } - - val enumConstants = enumC3559qCClass.enumConstants - ?: enumC3559qCClass.getMethod("values").invoke(null) as? Array<*> - ?: run { - context.log.error("Could not retrieve enum constants from ${enumC3559qCClass.name}") - context.longToast("Failed to add friend: Enum constants not accessible") - return@runCatching - } - - context.log.verbose("Found ${enumConstants.size} enum constants") - - val addedByUsername = enumConstants.firstOrNull { it.toString() == "ADDED_BY_USERNAME" } - ?: enumConstants.firstOrNull { it.toString().contains("USERNAME", ignoreCase = true) } - ?: enumConstants.firstOrNull() - ?: run { - context.log.error("No enum constants available") - context.longToast("Failed to add friend: No valid enum constant found") - return@runCatching - } - - context.log.verbose("Using enum constant: $addedByUsername") - - val sQ7Default = loadStaticDefaultField(classLoader, "sQ7", "f288251e0") - val zQ7Default = loadStaticDefaultField(classLoader, "ZQ7", "f159958F0") - - val iBgClass = try { - classLoader.loadClass("iBg") - } catch (e: Exception) { - context.log.error("Could not load iBg class: ${e.message}") - null - } - - if (iBgClass == null) { - context.log.error("iBg class not found") - return@runCatching - } - - val m51157aMethod = iBgClass.declaredMethods.firstOrNull { it.name == "m51157a" } - ?: iBgClass.methods.firstOrNull { it.name == "m51157a" } - ?: iBgClass.declaredMethods.firstOrNull { method -> - java.lang.reflect.Modifier.isStatic(method.modifiers) && - method.parameterTypes.size == 14 && - method.parameterTypes[0].isAssignableFrom(friendRelationshipChangerInstance.javaClass) && - method.parameterTypes[1] == String::class.java && - method.parameterTypes[2] == enumC3559qCClass - } - - if (m51157aMethod == null) { - context.log.error("Could not find iBg.m51157a method") - context.log.error("Available static methods with 14 params:") - iBgClass.declaredMethods.filter { - java.lang.reflect.Modifier.isStatic(it.modifiers) && it.parameterTypes.size == 14 - }.forEach { method -> - context.log.error(" ${method.name}: ${method.parameterTypes.joinToString { it.simpleName }}") - } - return@runCatching - } - m51157aMethod.isAccessible = true - - try { - m51157aMethod.invoke( - null, - friendRelationshipChangerInstance, - userId, - addedByUsername, - sQ7Default, - zQ7Default, - null, - null, - null, - null, - null, - null, - null, - null, - 4064 - )?.also { result -> - result.javaClass.methods - .find { it.name == "subscribe" && it.parameterCount == 0 } - ?.apply { isAccessible = true } - ?.invoke(result) - } - } catch (e: Exception) { - context.log.error("Exception during method invocation: ${e.javaClass.name}: ${e.message}") - e.cause?.let { cause -> - context.log.error("Cause: ${cause.javaClass.name}: ${cause.message}") - cause.stackTrace?.take(5)?.forEach { - context.log.error(" at $it") - } - } - throw e - } - }.onFailure { - context.log.error("Failed to add friend $userId", it) - context.longToast("Failed to add friend: ${it.message}") - } - } - - private fun loadSuggestedFriends( - coroutineScope: CoroutineScope, - onLoaded: (List) -> Unit - ) { - coroutineScope.launch(Dispatchers.IO) { - val blacklist = getUserIdBlacklist() - val suggestedFriends = context.database.getAllFriends() - .filter { it.userId !in blacklist && it.friendLinkType == FriendLinkType.SUGGESTED.value } - .sortedByDescending { it.addedTimestamp } - .mapNotNull { it.userId } - withContext(Dispatchers.Main) { - onLoaded(suggestedFriends) - } - } - } - - override fun onActivityCreate() { - context.event.subscribe(ActivityResultEvent::class) { event -> - if (event.requestCode == pendingPickerAction?.first) { - val pendingAction = pendingPickerAction ?: return@subscribe - this.pendingPickerAction = null - event.canceled = true - pendingAction.second(event.intent.data!!) - } - } - } - - private fun exportFriends( - userIds: List - ) { - pendingPickerAction = Random.nextInt(0, 65535) to { data -> - context.androidContext.contentResolver.openOutputStream(data).use { output -> - output?.bufferedWriter()?.use { writer -> - userIds.forEach { - writer.write(it) - writer.newLine() - } - } - context.longToast("Exported ${userIds.size} friends!") - } - } - context.mainActivity?.startActivityForResult( - Intent.createChooser( - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TITLE, "my_friends.txt") - }, - "Select a location to save the file" - ), - pendingPickerAction!!.first - ) - } - - private val userIdToSnapchatter = mutableMapOf() + private val uuidRegex = Regex("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}") private fun getUserIdBlacklist() = arrayOf( context.database.myUserId, @@ -304,21 +83,131 @@ class ManageFriendList : AbstractAction() { "84ee8839-3911-492d-8b94-72dd80f3713a", ) + private fun addFriend(userId: String) { + val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance + ?: run { + context.longToast("Failed to add friend: FriendRelationshipChanger instance not available") + return + } + + runCatching { + val classLoader = context.androidContext.classLoader + val friendRelationshipChangerClass = friendRelationshipChangerInstance.javaClass + + // Helper function to find static field by trying multiple field names + fun findStaticField(clazz: Class<*>, fieldNames: List): Any? { + fieldNames.forEach { fieldName -> + runCatching { + clazz.getDeclaredField(fieldName).apply { isAccessible = true }.get(null) + }.getOrNull()?.let { return it } + } + // Fallback: find any static field of the same type + return clazz.declaredFields.firstOrNull { field -> + java.lang.reflect.Modifier.isStatic(field.modifiers) && field.type == clazz + }?.let { field -> + runCatching { + field.isAccessible = true + field.get(null)?.takeIf { it.javaClass == clazz } + }.getOrNull() + } + } + + // Load F9l class + val f9lClass = classLoader.loadClass("F9l") + + // Find the add friend method by matching signature + val method = (f9lClass.declaredMethods + f9lClass.methods).firstOrNull { method -> + if (!java.lang.reflect.Modifier.isStatic(method.modifiers) || method.parameterTypes.size != 14) return@firstOrNull false + + val params = method.parameterTypes + params[0].isAssignableFrom(friendRelationshipChangerClass) && + params[1] == String::class.java && + params[2].isEnum + } ?: return@runCatching context.log.error("Could not find F9l.m8344a method") + + // Extract classes from method signature + val enumClass = method.parameterTypes[2] + val tZ7Class = method.parameterTypes[3] + val g08Class = method.parameterTypes[4] + + // Get enum constant for USERNAME + val enumConstants = enumClass.enumConstants ?: enumClass.getMethod("values").invoke(null) as? Array<*> + ?: return@runCatching context.log.error("Could not get enum constants") + val addedByUsername = enumConstants.firstOrNull { it.toString().contains("USERNAME", ignoreCase = true) } + ?: return@runCatching context.log.error("Could not find ADDED_BY_USERNAME enum") + + // Get static field instances + val tZ7Default = findStaticField(tZ7Class, listOf("f301161j0", "f301158a", "f301165n0", "f301160c", "f301171t")) + ?: return@runCatching context.log.error("Could not find tZ7 static field") + + val g08Default = findStaticField(g08Class, listOf("f211750K0", "f211807x1", "f211805w1", "f211774f1", "f211775g1", "f211760U0", "f211783l1", "f211785m1", "f211787n1", "f211772d1")) + ?: return@runCatching context.log.error("Could not find g08 static field") + + // Invoke method with 14 parameters + method.isAccessible = true + val result = method.invoke( + null, + friendRelationshipChangerInstance, + userId, + addedByUsername, + tZ7Default, + g08Default, + null, null, null, null, null, // String params 6-10 + null, // InteractionPlacementInfo + null, // String str7 + null, // Integer num + 4064 // int flags + ) + + // Subscribe to the Completable result + result?.javaClass?.methods?.firstOrNull { + it.name == "subscribe" && it.parameterCount == 0 + }?.let { subscribeMethod -> + subscribeMethod.isAccessible = true + subscribeMethod.invoke(result) + } + }.onFailure { + context.log.error("Failed to add friend $userId", it) + context.longToast("Failed to add friend: ${it.message}") + } + } + + override fun onActivityCreate() { + context.event.subscribe(ActivityResultEvent::class) { event -> + pendingPickerAction?.takeIf { it.first == event.requestCode }?.let { + pendingPickerAction = null + event.canceled = true + it.second(event.intent.data!!) + } + } + } + + private fun exportFriends(userIds: List) { + pendingPickerAction = Random.nextInt(0, 65535) to { data -> + context.androidContext.contentResolver.openOutputStream(data)?.bufferedWriter()?.use { writer -> + userIds.forEach { writer.write(it); writer.newLine() } + } + context.longToast("Exported ${userIds.size} friends!") + } + context.mainActivity?.startActivityForResult( + Intent.createChooser(Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TITLE, "my_friends.txt") + }, "Select a location to save the file"), + pendingPickerAction!!.first + ) + } + + private val userIdToSnapchatter = mutableMapOf() + @Composable private fun ManagerDialog() { val pendingFriendRequests = remember { mutableStateMapOf() } var fetchedFriends by remember { mutableStateOf?>(null) } val coroutineScope = rememberCoroutineScope() - val openSuggestedOnLaunch = remember { consumeOpenSuggestedOnLaunch() } - val bitmojiCache = remember { me.eternal.purrfectsnap.core.util.EvictingMap(50) } + val bitmojiCache = remember { EvictingMap(50) } val noBitmojiBitmap = remember { BitmapFactory.decodeResource(context.resources, android.R.drawable.ic_menu_report_image).asImageBitmap() } - LaunchedEffect(openSuggestedOnLaunch) { - if (openSuggestedOnLaunch) { - loadSuggestedFriends(coroutineScope) { fetchedFriends = it } - } - } - Box( modifier = Modifier .fillMaxWidth() @@ -425,50 +314,38 @@ class ManageFriendList : AbstractAction() { ) { pendingPickerAction = Random.nextInt(0, 65535) to { data -> runCatching { - fetchedFriends = null - context.androidContext.contentResolver.openInputStream(data).use { input -> - fetchedFriends = input?.bufferedReader()?.readLines()?.filter { - it.matches(uuidRegex) - }?.map { it.trim() }?.toMutableList() ?: mutableListOf() - } + fetchedFriends = context.androidContext.contentResolver.openInputStream(data)?.bufferedReader()?.readLines()?.filter { it.matches(uuidRegex) }?.map { it.trim() }?.toMutableList() ?: mutableListOf() }.onFailure { context.log.error("Failed to import friends", it) context.longToast("Failed to import friends: ${it.message}") } } - context.mainActivity?.startActivityForResult( - Intent.createChooser( - Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" }, - "Select a file" - ), - pendingPickerAction!!.first - ) + context.mainActivity?.startActivityForResult(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" }, "Select a file"), pendingPickerAction!!.first) } } PrimaryButton( - text = "Load Suggested Friends", + text = translation.get("load_suggested_friends"), modifier = Modifier.fillMaxWidth() ) { - loadSuggestedFriends(coroutineScope) { fetchedFriends = it } + coroutineScope.launch(Dispatchers.IO) { + val blacklist = getUserIdBlacklist() + val suggestedFriends = context.database.getAllFriends().filter { it.userId !in blacklist && it.friendLinkType == FriendLinkType.SUGGESTED.value }.sortedByDescending { it.addedTimestamp }.mapNotNull { it.userId } + withContext(Dispatchers.Main) { fetchedFriends = suggestedFriends } + } } } } else { var searchQuery by remember { mutableStateOf("") } - + val filteredFriends = remember(fetchedFriends, searchQuery) { val friends = fetchedFriends ?: emptyList() - if (searchQuery.isBlank()) { - friends.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L } - } else { - friends.filter { userId -> - val friendInfo = context.database.getFriendInfo(userId) - friendInfo?.mutableUsername?.contains(searchQuery, ignoreCase = true) == true || - friendInfo?.displayName?.contains(searchQuery, ignoreCase = true) == true || - userId.contains(searchQuery, ignoreCase = true) - }.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L } - } + val sorted = { list: List -> list.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L } } + if (searchQuery.isBlank()) sorted(friends) else sorted(friends.filter { userId -> + val info = context.database.getFriendInfo(userId) + info?.mutableUsername?.contains(searchQuery, ignoreCase = true) == true || info?.displayName?.contains(searchQuery, ignoreCase = true) == true || userId.contains(searchQuery, ignoreCase = true) + }) } - + Column( modifier = Modifier .fillMaxWidth() @@ -511,7 +388,7 @@ class ManageFriendList : AbstractAction() { } Spacer(modifier = Modifier.size(46.dp)) } - + BasicTextField( value = searchQuery, onValueChange = { searchQuery = it }, @@ -572,64 +449,52 @@ class ManageFriendList : AbstractAction() { } } items(filteredFriends) { userId -> - val friendInfo = remember(userId) { context.database.getFriendInfo(userId) } - val linkType = remember(friendInfo) { - friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) } - } - val isActuallyAdded = remember(friendInfo, linkType) { - friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 && - (linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING) - } + var friendInfo by remember(userId) { mutableStateOf(context.database.getFriendInfo(userId)) } + var friendLinkType by remember(userId) { mutableStateOf(friendInfo?.let { FriendLinkType.fromValue(it.friendLinkType) }) } + + val isActuallyAdded = friendInfo?.let { info -> + friendLinkType != null && info.addedTimestamp > 0 && + (friendLinkType == FriendLinkType.MUTUAL || friendLinkType == FriendLinkType.OUTGOING) + } ?: false var friendSnapchatter by remember(userId) { mutableStateOf(null) } - var friendLinkType by remember(userId) { mutableStateOf(linkType) } - var actuallyAdded by remember(userId) { mutableStateOf(isActuallyAdded) } - - var bitmojiBitmap by remember(userId, friendInfo?.bitmojiAvatarId) { - mutableStateOf(friendInfo?.bitmojiAvatarId?.let { bitmojiCache[it] }) - } LaunchedEffect(userId) { - if (friendSnapchatter == null && !userIdToSnapchatter.containsKey(userId)) { + friendSnapchatter = userIdToSnapchatter[userId] ?: run { withContext(Dispatchers.IO) { - context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull()?.let { + context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull()?.also { userIdToSnapchatter[userId] = it - friendSnapchatter = it } } - } else { - friendSnapchatter = userIdToSnapchatter[userId] - } - - while (true) { - delay(2000) - val newLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) } - if (newLinkType != friendLinkType) { - friendLinkType = newLinkType - } - val newActuallyAdded = friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 && - (linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING) - if (newActuallyAdded != actuallyAdded) { - actuallyAdded = newActuallyAdded - } } } - + + LaunchedEffect(Unit) { + while (true) { + delay(2000) + context.database.getFriendInfo(userId)?.let { + friendInfo = it + FriendLinkType.fromValue(it.friendLinkType)?.let { newType -> + if (newType != friendLinkType) friendLinkType = newType + } + } + } + } + + var bitmojiBitmap by remember(userId, friendInfo?.bitmojiAvatarId) { + mutableStateOf(friendInfo?.bitmojiAvatarId?.let { bitmojiCache[it] }) + } + LaunchedEffect(userId, friendInfo?.bitmojiAvatarId, friendInfo?.bitmojiSelfieId) { - if (bitmojiBitmap != null || friendInfo?.bitmojiAvatarId == null || friendInfo?.bitmojiSelfieId == null) return@LaunchedEffect - - withContext(Dispatchers.IO) { - val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie( - friendInfo.bitmojiSelfieId, - friendInfo.bitmojiAvatarId, - BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D - ) ?: return@withContext - - runCatching { - RemoteMediaResolver.downloadMedia(bitmojiUrl) { inputStream, length -> - val avatarId = friendInfo.bitmojiAvatarId ?: return@downloadMedia - bitmojiCache[avatarId] = BitmapFactory.decodeStream(inputStream).also { - bitmojiBitmap = it + val info = friendInfo ?: return@LaunchedEffect + val avatarId = info.bitmojiAvatarId ?: return@LaunchedEffect + val selfieId = info.bitmojiSelfieId ?: return@LaunchedEffect + if (bitmojiBitmap != null) return@LaunchedEffect + BitmojiSelfie.getBitmojiSelfie(selfieId, avatarId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D)?.let { url -> + withContext(Dispatchers.IO) { + runCatching { + RemoteMediaResolver.downloadMedia(url) { inputStream, _ -> + bitmojiCache[avatarId] = BitmapFactory.decodeStream(inputStream).also { bitmojiBitmap = it } } } } @@ -652,7 +517,7 @@ class ManageFriendList : AbstractAction() { contentDescription = null, modifier = Modifier.size(35.dp) ) - + Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp) @@ -680,39 +545,38 @@ class ManageFriendList : AbstractAction() { color = if (type == FriendLinkType.MUTUAL) Color(0xFF8EF0F3) else Color(0xFFD9D3FF) ) } - if (friendSnapchatter != null) { - val isPending = pendingFriendRequests.containsKey(userId) && pendingFriendRequests[userId]?.isActive != false + friendSnapchatter?.let { val isFollowing = friendLinkType == FriendLinkType.FOLLOWING + val isPending = pendingFriendRequests[userId]?.isActive == true + val canAdd = !isActuallyAdded && !isPending && !isFollowing PrimaryButton( text = when { isFollowing -> "Following" - actuallyAdded -> context.translation["common.added"] - isPending -> translation.get("adding") ?: "Adding..." + isActuallyAdded -> context.translation["common.added"] + isPending -> translation.get("adding") else -> translation.get("add") }, modifier = Modifier.widthIn(min = 110.dp), - enabled = !actuallyAdded && !isPending && !isFollowing + enabled = canAdd ) { - if (actuallyAdded || isPending || isFollowing) return@PrimaryButton - + if (!canAdd) return@PrimaryButton val prevLinkType = friendLinkType addFriend(userId) - val job = coroutineScope.launch { + pendingFriendRequests[userId] = coroutineScope.launch(Dispatchers.IO) { withTimeout(10000) { - while (friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }?.value == prevLinkType?.value) { + while (true) { + context.database.getFriendInfo(userId)?.let { updated -> + FriendLinkType.fromValue(updated.friendLinkType)?.takeIf { it != prevLinkType }?.let { + friendInfo = updated + friendLinkType = it + return@withTimeout + } + } delay(500) } } - }.apply { - invokeOnCompletion { - pendingFriendRequests.remove(userId) - friendLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) } - actuallyAdded = isActuallyAdded || (friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 && - (linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING)) - } - } - pendingFriendRequests[userId] = job + }.apply { invokeOnCompletion { pendingFriendRequests.remove(userId) } } } if (isPending) { CircularProgressIndicator( @@ -805,4 +669,4 @@ class ManageFriendList : AbstractAction() { } } } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/PreventMessageSending.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/PreventMessageSending.kt index e9d7d3a5..3610a4cb 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/PreventMessageSending.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/PreventMessageSending.kt @@ -2,7 +2,9 @@ package me.eternal.purrfectsnap.core.features.impl.messaging import me.eternal.purrfectsnap.common.data.NotificationType import me.eternal.purrfectsnap.common.util.protobuf.ProtoEditor +import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader import me.eternal.purrfectsnap.core.event.events.impl.NativeUnaryCallEvent +import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent import me.eternal.purrfectsnap.core.features.Feature import me.eternal.purrfectsnap.core.util.hook.HookStage @@ -12,9 +14,9 @@ class PreventMessageSending : Feature("Prevent message sending") { override fun init() { val preventMessageSending by context.config.messaging.preventMessageSending - context.event.subscribe(NativeUnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event -> - if (event.uri != "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") return@subscribe - event.buffer = ProtoEditor(event.buffer).apply { + fun handleUpdateContentMessage(uri: String, buffer: ByteArray): ByteArray? { + if (uri != "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") return null + return ProtoEditor(buffer).apply { edit(3) { // replace replayed to read receipt if (firstOrNull(13) != null) { @@ -25,6 +27,36 @@ class PreventMessageSending : Feature("Prevent message sending") { }.toByteArray() } + fun handleCreateContentMessage(uri: String, buffer: ByteArray): Boolean { + if (uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return false + val reader = ProtoReader(buffer) + // check for missed audio/video call in MessageContent (field 4) + val contentReader = reader.followPath(4) ?: return false + + // try both possible field IDs based on SnapEnums and ContentType.java + val contentType = contentReader.getVarInt(2) + + val isMissedAudio = contentType == 13L || contentType == 18L + val isMissedVideo = contentType == 12L || contentType == 17L + + if (isMissedAudio && preventMessageSending.contains("abandon_audio")) return true + if (isMissedVideo && preventMessageSending.contains("abandon_video")) return true + + return false + } + + arrayOf(NativeUnaryCallEvent::class, UnaryCallEvent::class).forEach { eventClass -> + context.event.subscribe(eventClass) { event -> + val uri = if (event is NativeUnaryCallEvent) event.uri else (event as UnaryCallEvent).uri + if (!uri.startsWith("/messagingcoreservice.MessagingCoreService/")) return@subscribe + + if (handleCreateContentMessage(uri, event.buffer)) { + event.canceled = true + } + handleUpdateContentMessage(uri, event.buffer)?.let { event.buffer = it } + } + } + context.classCache.conversationManager.hook("updateMessage", HookStage.BEFORE) { param -> val messageUpdate = param.arg(2).toString() if (messageUpdate == "SCREENSHOT" && preventMessageSending.contains("chat_screenshot")) { @@ -34,6 +66,10 @@ class PreventMessageSending : Feature("Prevent message sending") { if (messageUpdate == "SCREEN_RECORD" && preventMessageSending.contains("chat_screen_record")) { param.setResult(null) } + + if ((messageUpdate == "REPLAY" || messageUpdate == "replay") && preventMessageSending.contains("snap_replay")) { + param.setResult(null) + } } context.event.subscribe(SendMessageWithContentEvent::class) { event -> @@ -41,9 +77,8 @@ class PreventMessageSending : Feature("Prevent message sending") { val associatedType = NotificationType.fromContentType(contentType ?: return@subscribe) ?: return@subscribe if (preventMessageSending.contains(associatedType.key)) { - context.log.verbose("Preventing message sending for $associatedType") event.canceled = true } } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt index 90e1522f..fe30a946 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt @@ -94,6 +94,7 @@ class MessageSender( it.conversations = conversations.toCollection(ArrayList()) it.mPhoneNumbers = arrayListOf() it.stories = arrayListOf() + it.massSnaps = arrayListOf() } sendMessageWithContentMethod.invoke(context.feature(Messaging::class).conversationManager?.instanceNonNull(), messageDestinations.instanceNonNull(), localMessageContent, callback) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/SnapUUID.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/SnapUUID.kt index 6dff682d..12effc3d 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/SnapUUID.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/SnapUUID.kt @@ -1,6 +1,6 @@ package me.eternal.purrfectsnap.core.wrapper.impl -import me.eternal.purrfectsnap.core.PurrfectSnap +import me.eternal.purrfectsnap.core.purrfectsnap import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper import java.nio.ByteBuffer @@ -29,12 +29,14 @@ class SnapUUID( obj } obj is UUID -> obj.toBytes() - PurrfectSnap.classCache.snapUUID.isInstance(obj) -> { + purrfectsnap.classCache.snapUUID.isInstance(obj) -> { obj?.getObjectField("mId") as ByteArray } - PurrfectSnap.classCache.snapShimsUUID?.isInstance(obj) == true -> { + purrfectsnap.classCache.snapShimsUUID?.isInstance(obj) == true -> { val any = obj as Any - any::class.java.methods.firstOrNull { it.name == "getId" }?.invoke(any) as? ByteArray ?: ByteArray(16) + runCatching { any.javaClass.getMethod("getId").invoke(any) as ByteArray }.getOrElse { + any.getObjectField("mId") as ByteArray + } } else -> ByteArray(16) } @@ -44,7 +46,7 @@ class SnapUUID( override var instance: Any? set(_) {} - get() = PurrfectSnap.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes) + get() = purrfectsnap.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes) override fun toString(): String { return uuidString @@ -60,3 +62,4 @@ class SnapUUID( return uuidBytes.contentHashCode() } } + diff --git a/native/rust/Cargo.lock b/native/rust/Cargo.lock index d2e497fb..8e011a48 100644 --- a/native/rust/Cargo.lock +++ b/native/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" @@ -21,24 +21,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_log-sys" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" [[package]] name = "android_logger" @@ -62,9 +56,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -74,15 +68,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -95,9 +89,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -107,9 +101,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bzip2" @@ -133,10 +127,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.9" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -150,9 +145,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -162,14 +157,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -221,9 +215,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -268,7 +262,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.114", ] [[package]] @@ -343,9 +337,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -353,12 +347,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -368,10 +362,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "flate2" -version = "1.0.35" +name = "find-msvc-tools" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", @@ -389,15 +389,38 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "goblin" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db6758c546e6f81f265638c980e5e84dfbda80cfd8e89e02f83454c8e8124bd" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "goldberg" version = "0.1.0" @@ -428,14 +451,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -460,9 +484,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jni" @@ -488,18 +512,19 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -507,9 +532,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" @@ -519,23 +544,24 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "log" -version = "0.4.25" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -578,9 +604,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "password-hash" @@ -623,9 +649,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "powerfmt" @@ -635,18 +667,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -686,6 +718,7 @@ dependencies = [ "crc32fast", "dobby-rs", "ed25519-dalek", + "goblin", "goldberg", "jni", "libc", @@ -699,18 +732,24 @@ dependencies = [ "serde_json", "sha2", "zip", - "zstd 0.13.2", + "zstd 0.13.3", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -738,14 +777,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -755,9 +794,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -766,9 +805,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rustc_version" @@ -781,9 +820,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", @@ -794,15 +833,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -813,6 +846,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "semver" version = "1.0.27" @@ -821,34 +874,45 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -888,6 +952,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "spki" version = "0.7.3" @@ -917,9 +987,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -943,27 +1013,27 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.114", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "typenum" @@ -973,9 +1043,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "version_check" @@ -995,41 +1065,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1037,42 +1103,92 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.96", - "wasm-bindgen-backend", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -1093,6 +1209,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -1215,24 +1340,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "zerocopy" -version = "0.7.35" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.114", ] [[package]] @@ -1261,6 +1391,12 @@ dependencies = [ "zstd 0.11.2+zstd.1.5.2", ] +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -1272,11 +1408,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe 7.2.1", + "zstd-safe 7.2.4", ] [[package]] @@ -1291,18 +1427,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.1" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/native/rust/Cargo.toml b/native/rust/Cargo.toml index 89fcff97..3ce4ffee 100644 --- a/native/rust/Cargo.toml +++ b/native/rust/Cargo.toml @@ -27,6 +27,7 @@ sha2 = "0.10" zip = "0.6.6" base64 = "0.22.1" ed25519-dalek = { version = "2.1.1", default-features = true, features = ["std"] } +goblin = "0.10.4" [build-dependencies] cc = "1.0" diff --git a/native/rust/src/common.rs b/native/rust/src/common.rs index 6e4033c8..5649f2d2 100644 --- a/native/rust/src/common.rs +++ b/native/rust/src/common.rs @@ -24,11 +24,16 @@ pub static CLIENT_MODULE: Lazy = Lazy::new(|| { pub fn set_native_lib_instance(instance: GlobalRef) { - NATIVE_LIB_INSTANCE.set(instance).expect("NativeLib instance already set"); + NATIVE_LIB_INSTANCE + .set(instance) + .expect("NativeLib instance already set"); } pub fn native_lib_instance() -> GlobalRef { - NATIVE_LIB_INSTANCE.get().expect("NativeLib instance not set").clone() + NATIVE_LIB_INSTANCE + .get() + .expect("NativeLib instance not set") + .clone() } pub fn set_java_vm(vm: *mut jni::sys::JavaVM) { @@ -37,13 +42,16 @@ pub fn set_java_vm(vm: *mut jni::sys::JavaVM) { pub fn java_vm() -> JavaVM { unsafe { - JavaVM::from_raw(*JAVA_VM.get().expect("JavaVM not set") as *mut jni::sys::JavaVM).expect("Failed to get JavaVM") + JavaVM::from_raw(*JAVA_VM.get().expect("JavaVM not set") as *mut jni::sys::JavaVM) + .expect("Failed to get JavaVM") } } pub fn attach_jni_env(block: impl FnOnce(&mut jni::JNIEnv)) { let jvm = java_vm(); - let mut env: jni::AttachGuard = jvm.attach_current_thread().expect("Failed to attach to current thread"); + let mut env: jni::AttachGuard = jvm + .attach_current_thread() + .expect("Failed to attach to current thread"); block(&mut env); -} \ No newline at end of file +} diff --git a/native/rust/src/config.rs b/native/rust/src/config.rs index ab781640..ee549418 100644 --- a/native/rust/src/config.rs +++ b/native/rust/src/config.rs @@ -1,11 +1,16 @@ -use std::{error::Error, sync::Mutex}; -use jni::{objects::JObject, JNIEnv}; use crate::{secstrings, util::get_jni_string}; +use jni::{objects::JObject, JNIEnv}; +use std::{error::Error, sync::Mutex}; static NATIVE_CONFIG: Mutex> = Mutex::new(None); pub fn native_config() -> NativeConfig { - NATIVE_CONFIG.lock().unwrap().as_ref().expect("NativeConfig not loaded").clone() + NATIVE_CONFIG + .lock() + .unwrap() + .as_ref() + .expect("NativeConfig not loaded") + .clone() } #[derive(Debug, Clone)] @@ -27,11 +32,13 @@ impl NativeConfig { macro_rules! get_string { ($field:expr) => { match env.get_field(&obj, $field, "Ljava/lang/String;")?.l()? { - jstring => if !jstring.is_null() { - Some(get_jni_string(env, jstring.into())?) - } else { - None - }, + jstring => { + if !jstring.is_null() { + Some(get_jni_string(env, jstring.into())?) + } else { + None + } + } } }; } @@ -73,10 +80,11 @@ pub fn get_blocker_config() -> BlockerConfig { } } -pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) { - NATIVE_CONFIG.lock().unwrap().replace( - NativeConfig::new(&mut env, obj).expect("Failed to load NativeConfig") - ); - +pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) { + NATIVE_CONFIG + .lock() + .unwrap() + .replace(NativeConfig::new(&mut env, obj).expect("Failed to load NativeConfig")); + info!("Config loaded {:?}", native_config()); } diff --git a/native/rust/src/hook.rs b/native/rust/src/hook.rs index 7527e4bd..8f48ae3c 100644 --- a/native/rust/src/hook.rs +++ b/native/rust/src/hook.rs @@ -36,15 +36,3 @@ macro_rules! dobby_hook { }; } -#[macro_export] -macro_rules! dobby_hook_sym { - ($lib:expr, $sym:expr, $hook:expr) => { - if let Some(hook_symbol) = dobby_rs::resolve_symbol($lib, $sym) { - crate::dobby_hook!(hook_symbol, $hook); - debug!("hooked symbol: {}", $sym); - } else { - panic!("Failed to resolve symbol: {}", $sym); - } - }; -} - diff --git a/native/rust/src/lib.rs b/native/rust/src/lib.rs index 0aeccbd6..9ac59f16 100644 --- a/native/rust/src/lib.rs +++ b/native/rust/src/lib.rs @@ -3,11 +3,11 @@ extern crate log; mod common; -mod hook; -mod util; -mod mapped_lib; mod config; +mod hook; +mod mapped_lib; mod sig; +mod util; mod modules; mod security; @@ -15,7 +15,10 @@ mod secstrings; use android_logger::Config; use log::LevelFilter; -use modules::{valdi_hook, custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook}; +use modules::{ + custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook, + valdi_hook, +}; use jni::{JNIEnv, JavaVM, NativeMethod}; use jni::objects::{JObject, JString, JClass, JValue}; @@ -166,10 +169,11 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring { let start_time = std::time::Instant::now(); // load signature cache - + if !signature_cache.is_null() { - let sig_cache_str = util::get_jni_string(&mut env, signature_cache).expect("Failed to convert mappings to string"); - + let sig_cache_str = util::get_jni_string(&mut env, signature_cache) + .expect("Failed to convert mappings to string"); + if let Ok(signature_cache) = serde_json::from_str(sig_cache_str.as_str()) { sig::add_signatures(signature_cache); } else { @@ -177,7 +181,11 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring { } } - common::set_native_lib_instance(env.new_global_ref(_class).ok().expect("Failed to create global ref")); + common::set_native_lib_instance( + env.new_global_ref(_class) + .ok() + .expect("Failed to create global ref"), + ); let _ = common::CLIENT_MODULE; diff --git a/native/rust/src/mapped_lib.rs b/native/rust/src/mapped_lib.rs index eadbd740..f0495efb 100644 --- a/native/rust/src/mapped_lib.rs +++ b/native/rust/src/mapped_lib.rs @@ -24,21 +24,24 @@ impl MappedLib { } pub fn search(&mut self) -> Result<&Self, Box> { - procfs::process::Process::myself()?.maps()?.iter().for_each(|map| { - let pathname = &map.pathname; + procfs::process::Process::myself()? + .maps()? + .iter() + .for_each(|map| { + let pathname = &map.pathname; - if let MMapPath::Path(path_buffer) = pathname { - let path = path_buffer.to_string_lossy(); + if let MMapPath::Path(path_buffer) = pathname { + let path = path_buffer.to_string_lossy(); - if path.contains(&self.name) { - self.regions.push(MappedRegion { - start: map.address.0, - end: map.address.1, - perms: map.perms, - }); + if path.contains(&self.name) { + self.regions.push(MappedRegion { + start: map.address.0, + end: map.address.1, + perms: map.perms, + }); + } } - } - }); + }); if self.regions.is_empty() { return Err(format!("No regions found for {}", self.name).into()); diff --git a/native/rust/src/modules/custom_font_hook.rs b/native/rust/src/modules/custom_font_hook.rs index d26cfb2d..7fa1a226 100644 --- a/native/rust/src/modules/custom_font_hook.rs +++ b/native/rust/src/modules/custom_font_hook.rs @@ -2,33 +2,41 @@ use std::{ffi::CStr, fs}; use nix::libc::{self, c_uint}; -use crate::{config, def_hook, dobby_hook_sym}; +use crate::{config, def_hook, dobby_hook, modules::util::elf}; -def_hook!( - open_hook, - i32, - |path: *const u8, flags: i32, mode: c_uint| { - if let Ok(pathname) = CStr::from_ptr(path).to_str() { - 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); - } else { - warn!("custom emoji font path does not exist: {}", font_path); - } +def_hook!(open_hook, i32, |path: *const u8, + flags: i32, + mode: c_uint| { + if let Ok(pathname) = CStr::from_ptr(path).to_str() { + 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, + ); + } else { + warn!("custom emoji font path does not exist: {}", font_path); } } } - - open_hook_original.unwrap()(path, flags, mode) } -); + open_hook_original.unwrap()(path, flags, mode) +}); pub fn init() { if config::native_config().custom_emoji_font_path.is_none() { return; } - dobby_hook_sym!("libc.so", "open", open_hook); -} \ No newline at end of file + let libc = elf::Elf::from_maps("/libc.so").expect("Failed to find libc.so in maps"); + + if let Some(ptr) = libc.get_symbol_address("open") { + dobby_hook!(ptr as _, open_hook); + } else { + panic!("Failed to find open symbol"); + } +} diff --git a/native/rust/src/modules/duplex_hook.rs b/native/rust/src/modules/duplex_hook.rs index b73dac3b..85478759 100644 --- a/native/rust/src/modules/duplex_hook.rs +++ b/native/rust/src/modules/duplex_hook.rs @@ -4,7 +4,6 @@ use jni::{objects::JObject, sys::jboolean, JNIEnv}; use crate::{common, def_hook, dobby_hook, util::get_jni_string}; - def_hook!( is_same_object, jboolean, @@ -20,9 +19,15 @@ def_hook!( if !env.is_instance_of(&obj1, class).unwrap() { return is_same_object_original.unwrap()(env, obj1, obj2); } - - let obj1_class_name = env.call_method(&obj1, "getName", "()Ljava/lang/String;", &[]).unwrap().l().unwrap().into(); - let class_name = get_jni_string(&mut env, obj1_class_name).expect("Failed to get class name"); + + let obj1_class_name = env + .call_method(&obj1, "getName", "()Ljava/lang/String;", &[]) + .unwrap() + .l() + .unwrap() + .into(); + let class_name = + get_jni_string(&mut env, obj1_class_name).expect("Failed to get class name"); if class_name.contains("com.snapchat.client.duplex.MessageHandler") { debug!("is_same_object hook: MessageHandler"); @@ -33,9 +38,11 @@ def_hook!( } ); - pub fn init() { common::attach_jni_env(|env| { - dobby_hook!((**env.get_native_interface()).IsSameObject.unwrap() as *mut c_void, is_same_object); + dobby_hook!( + (**env.get_native_interface()).IsSameObject.unwrap() as *mut c_void, + is_same_object + ); }); -} \ No newline at end of file +} diff --git a/native/rust/src/modules/fstat_hook.rs b/native/rust/src/modules/fstat_hook.rs index c838df8f..2ba44d1c 100644 --- a/native/rust/src/modules/fstat_hook.rs +++ b/native/rust/src/modules/fstat_hook.rs @@ -1,44 +1,48 @@ +use crate::{ + config::{self, native_config}, + def_hook, dobby_hook, + modules::util::elf, +}; +use nix::libc; use std::fs; -use nix::libc; +def_hook!(fstat_hook, i32, |fd: i32, statbuf: *mut libc::stat| { + if let Ok(link) = fs::read_link("/proc/self/fd/".to_owned() + &fd.to_string()) { + let link_str = link.to_string_lossy(); + let config = native_config(); + if config.disable_metrics && link_str.contains("files/blizzardv2/queues") { + if libc::unlink((link_str.to_string() + "\0").as_ptr()) == -1 { + warn!("Failed to unlink {}", link_str); + } + return -1; + } -use crate::{config::{self, native_config}, def_hook, dobby_hook_sym}; - -def_hook!( - fstat_hook, - i32, - |fd: i32, statbuf: *mut libc::stat| { - if let Ok(link) = fs::read_link("/proc/self/fd/".to_owned() + &fd.to_string()) { - let link_str = link.to_string_lossy(); - let config = native_config(); - if config.disable_metrics && link_str.contains("files/blizzardv2/queues") { - if libc::unlink((link_str.to_string() + "\0").as_ptr()) == -1 { - warn!("Failed to unlink {}", link_str); - } + if config.disable_bitmoji { + if link_str.contains("com.snap.file_manager_4_SCContent") { return -1; } - - if config.disable_bitmoji { - if link_str.contains("com.snap.file_manager_4_SCContent") { + if link_str.contains("/files/file_manager/") { + let lower = link_str.to_lowercase(); + if lower.contains("bitmoji") { return -1; } - if link_str.contains("/files/file_manager/") { - let lower = link_str.to_lowercase(); - if lower.contains("bitmoji") { - return -1; - } - } } } - - fstat_hook_original.unwrap()(fd, statbuf) } -); + + fstat_hook_original.unwrap()(fd, statbuf) +}); pub fn init() { let config = config::native_config(); if config.disable_metrics || config.disable_bitmoji { - dobby_hook_sym!("libc.so", "fstat", fstat_hook); + let libc = elf::Elf::from_maps("/libc.so").expect("Failed to find libc.so in maps"); + + if let Some(ptr) = libc.get_symbol_address("fstat") { + dobby_hook!(ptr as _, fstat_hook); + } else { + panic!("Failed to find fstat symbol"); + } } -} \ No newline at end of file +} diff --git a/native/rust/src/modules/linker_hook.rs b/native/rust/src/modules/linker_hook.rs index b73ede33..02627ac3 100644 --- a/native/rust/src/modules/linker_hook.rs +++ b/native/rust/src/modules/linker_hook.rs @@ -1,12 +1,20 @@ -use std::{collections::HashMap, ffi::{c_void, CStr}, sync::Mutex}; +use std::{ + collections::HashMap, + ffi::{c_void, CStr}, + sync::Mutex, +}; -use jni::{objects::{JByteArray, JString}, JNIEnv}; +use jni::{ + objects::{JByteArray, JString}, + JNIEnv, +}; use nix::libc; use once_cell::sync::Lazy; -use crate::{def_hook, dobby_hook_sym}; +use crate::{def_hook, dobby_hook, modules::util::elf}; -static SHARED_LIBRARIES: Lazy>>>> = Lazy::new(|| Mutex::new(HashMap::new())); +static SHARED_LIBRARIES: Lazy>>>> = + Lazy::new(|| Mutex::new(HashMap::new())); def_hook!( linker_openat, @@ -17,8 +25,13 @@ def_hook!( if let Some(content) = SHARED_LIBRARIES.lock().unwrap().remove(&pathname_str) { let memfd = libc::syscall(libc::SYS_memfd_create, "jit-cache\0".as_ptr(), 0) as i32; let content = content.into_boxed_slice(); - - if libc::write(memfd, content.as_ptr() as *const c_void, content.len() as libc::size_t) == -1 { + + if libc::write( + memfd, + content.as_ptr() as *const c_void, + content.len() as libc::size_t, + ) == -1 + { panic!("failed to write to memfd"); } @@ -36,25 +49,44 @@ def_hook!( } ); -pub fn add_linker_shared_library(mut env: JNIEnv, _: *mut c_void, path: JString, content: JByteArray) { +pub fn add_linker_shared_library( + mut env: JNIEnv, + _: *mut c_void, + path: JString, + content: JByteArray, +) { let path = env.get_string(&path).unwrap().to_str().unwrap().to_string(); - let content_length = env.get_array_length(&content).expect("Failed to get array length"); + let content_length = env + .get_array_length(&content) + .expect("Failed to get array length"); let mut content_buffer = Box::new(vec![0i8; content_length as usize]); - - env.get_byte_array_region(content, 0, content_buffer.as_mut_slice()).expect("Failed to get byte array region"); - + + env.get_byte_array_region(content, 0, content_buffer.as_mut_slice()) + .expect("Failed to get byte array region"); + debug!("added shared library: {}", path); - SHARED_LIBRARIES.lock().unwrap().insert(path, content_buffer); + SHARED_LIBRARIES + .lock() + .unwrap() + .insert(path, content_buffer); } pub fn init() { - #[cfg(target_arch = "aarch64")] - { - dobby_hook_sym!("linker64", "__dl___openat", linker_openat); - } - #[cfg(target_arch = "arm")] - { - dobby_hook_sym!("linker", "__dl___openat", linker_openat); + let linker = { + #[cfg(target_arch = "aarch64")] + { + elf::Elf::from_maps("/linker64").expect("Failed to find linker64 in maps") + } + #[cfg(target_arch = "arm")] + { + elf::Elf::from_maps("/linker").expect("Failed to find linker in maps") + } + }; + + if let Some(ptr) = linker.get_symbol_address("__dl___openat") { + dobby_hook!(ptr as _, linker_openat); + } else { + panic!("Failed to find open symbol"); } } diff --git a/native/rust/src/modules/mod.rs b/native/rust/src/modules/mod.rs index 97f8f0ee..66661902 100644 --- a/native/rust/src/modules/mod.rs +++ b/native/rust/src/modules/mod.rs @@ -1,8 +1,8 @@ -pub mod util; -pub mod linker_hook; -pub mod duplex_hook; -pub mod sqlite_hook; -pub mod fstat_hook; -pub mod unary_call_hook; -pub mod valdi_hook; pub mod custom_font_hook; +pub mod duplex_hook; +pub mod fstat_hook; +pub mod linker_hook; +pub mod sqlite_hook; +pub mod unary_call_hook; +pub mod util; +pub mod valdi_hook; diff --git a/native/rust/src/modules/sqlite_hook.rs b/native/rust/src/modules/sqlite_hook.rs index c837ebf0..30925d23 100644 --- a/native/rust/src/modules/sqlite_hook.rs +++ b/native/rust/src/modules/sqlite_hook.rs @@ -1,25 +1,34 @@ -use std::{collections::HashMap, ffi::{c_void, CStr}, mem::size_of, ptr::addr_of_mut, sync::Mutex}; +use std::{ + collections::HashMap, + ffi::{c_void, CStr}, + mem::size_of, + ptr::addr_of_mut, + sync::Mutex, +}; -use jni::{objects::{JObject, JString}, JNIEnv}; +use jni::{ + objects::{JObject, JString}, + JNIEnv, +}; use nix::libc::{self, pthread_mutex_t}; use once_cell::sync::Lazy; use crate::{common, def_hook, dobby_hook, sig, util::get_jni_string}; - #[repr(C)] #[derive(Clone, Copy, Debug)] struct Sqlite3Mutex { - mutex: pthread_mutex_t + mutex: pthread_mutex_t, } #[repr(C)] struct Sqlite3 { pad: [u8; 3 * size_of::()], - mutex: *mut Sqlite3Mutex + mutex: *mut Sqlite3Mutex, } -static SQLITE3_MUTEX_MAP: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +static SQLITE3_MUTEX_MAP: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); def_hook!( sqlite3_open, @@ -31,27 +40,38 @@ def_hook!( let sqlite3_mutex = (**pp_db).mutex; if sqlite3_mutex != std::ptr::null_mut() { - let filename = CStr::from_ptr(filename).to_string_lossy().to_string().split("/").last().expect("Failed to get filename").to_string(); + let filename = CStr::from_ptr(filename) + .to_string_lossy() + .to_string() + .split("/") + .last() + .expect("Failed to get filename") + .to_string(); debug!("sqlite3_open hook {:?}", filename); - SQLITE3_MUTEX_MAP.lock().unwrap().insert( - filename, - (*sqlite3_mutex).mutex - ); + SQLITE3_MUTEX_MAP + .lock() + .unwrap() + .insert(filename, (*sqlite3_mutex).mutex); } } result - } + } ); - pub fn lock_database(mut env: JNIEnv, _: *mut c_void, filename: JString, runnable: JObject) { - let database_filename = get_jni_string(&mut env, filename).expect("Failed to get database filename"); - let mutex = SQLITE3_MUTEX_MAP.lock().unwrap().get(&database_filename).map(|mutex| *mutex); + let database_filename = + get_jni_string(&mut env, filename).expect("Failed to get database filename"); + let mutex = SQLITE3_MUTEX_MAP + .lock() + .unwrap() + .get(&database_filename) + .map(|mutex| *mutex); let call_runnable = || { - env.call_method(runnable, "run", "()V", &[]).expect("Failed to call run method"); + env.call_method(runnable, "run", "()V", &[]) + .expect("Failed to call run method"); }; if let Some(mut mutex) = mutex { @@ -71,16 +91,17 @@ pub fn lock_database(mut env: JNIEnv, _: *mut c_void, filename: JString, runnabl } } - pub fn init() { if let Some(signature) = sig::find_signature( - &common::CLIENT_MODULE, - "FF FF 00 A9 3F 00 00 F9", -0x3C, - "9A 46 90 46 78 44 89 46 05 68",-0xd + &common::CLIENT_MODULE, + "FF FF 00 A9 3F 00 00 F9", + -0x3C, + "9A 46 90 46 78 44 89 46 05 68", + -0xd, ) { debug!("Found sqlite3_open signature: {:#x}", signature); dobby_hook!(signature as *mut c_void, sqlite3_open); } else { - warn!("Failed to find sqlite3_open signature"); + panic!("Failed to find sqlite3_open signature"); } -} \ No newline at end of file +} diff --git a/native/rust/src/modules/unary_call_hook.rs b/native/rust/src/modules/unary_call_hook.rs index 872a7832..dd20e90e 100644 --- a/native/rust/src/modules/unary_call_hook.rs +++ b/native/rust/src/modules/unary_call_hook.rs @@ -1,17 +1,23 @@ use std::ffi::{c_void, CStr}; -use jni::{objects::{JByteArray, JMethodID, JValue}, signature::ReturnType}; +use jni::{ + objects::{JByteArray, JMethodID, JValue}, + signature::ReturnType, +}; use nix::libc; use once_cell::sync::OnceCell; -use crate::{common::{self}, def_hook, dobby_hook, sig}; +use crate::{ + common::{self}, + def_hook, dobby_hook, sig, +}; #[repr(C)] #[derive(Copy, Clone)] -struct RefCountedSliceByteBuffer { +struct SliceByteBuffer { ref_counter: *mut c_void, length: usize, - data: *mut u8 + data: *mut u8, } #[repr(C)] @@ -19,7 +25,7 @@ struct GrpcByteBuffer { reserved: *mut c_void, type_: *mut c_void, compression: *mut c_void, - slice_buffer: *mut RefCountedSliceByteBuffer + slice_buffer: *mut SliceByteBuffer, } static NATIVE_LIB_ON_UNARY_CALL_METHOD: OnceCell = OnceCell::new(); @@ -27,7 +33,12 @@ static NATIVE_LIB_ON_UNARY_CALL_METHOD: OnceCell = OnceCell::new(); def_hook!( unary_call, *mut c_void, - |unk1: *mut c_void, uri: *const u8, grpc_byte_buffer: *mut *mut GrpcByteBuffer, unk4: *mut c_void, unk5: *mut c_void, unk6: *mut c_void| { + |unk1: *mut c_void, + uri: *const u8, + grpc_byte_buffer: *mut *mut GrpcByteBuffer, + unk4: *mut c_void, + unk5: *mut c_void, + unk6: *mut c_void| { macro_rules! call_original { () => { unary_call_original.unwrap()(unk1, uri, grpc_byte_buffer, unk4, unk5, unk6) @@ -45,45 +56,73 @@ def_hook!( let mut env = java_vm.get_env().expect("Failed to get JNIEnv"); let slice_buffer_length = slice_buffer.length as usize; - let jni_buffer = env.new_byte_array(slice_buffer_length as i32).expect("Failed to create new byte array"); - env.set_byte_array_region(&jni_buffer, 0, std::slice::from_raw_parts(slice_buffer.data as *const i8, slice_buffer_length)).expect("Failed to set byte array region"); + let jni_buffer = env + .new_byte_array(slice_buffer_length as i32) + .expect("Failed to create new byte array"); + env.set_byte_array_region( + &jni_buffer, + 0, + std::slice::from_raw_parts(slice_buffer.data as *const i8, slice_buffer_length), + ) + .expect("Failed to set byte array region"); let uri_str = CStr::from_ptr(uri).to_str().unwrap(); - let native_request_data_object = env.call_method_unchecked( - common::native_lib_instance(), - NATIVE_LIB_ON_UNARY_CALL_METHOD.get().unwrap(), - ReturnType::Object, - &[ - JValue::from(&env.new_string(uri_str).unwrap()).as_jni(), - JValue::from(&jni_buffer).as_jni() - ] - ).expect("Failed to call onNativeUnaryCall method").l().unwrap(); + let native_request_data_object = env + .call_method_unchecked( + common::native_lib_instance(), + NATIVE_LIB_ON_UNARY_CALL_METHOD.get().unwrap(), + ReturnType::Object, + &[ + JValue::from(&env.new_string(uri_str).unwrap()).as_jni(), + JValue::from(&jni_buffer).as_jni(), + ], + ) + .expect("Failed to call onNativeUnaryCall method") + .l() + .unwrap(); if native_request_data_object.is_null() { return call_original!(); } - let is_canceled = env.get_field(&native_request_data_object, "canceled", "Z").expect("Failed to get canceled field").z().unwrap(); + let is_canceled = env + .get_field(&native_request_data_object, "canceled", "Z") + .expect("Failed to get canceled field") + .z() + .unwrap(); if is_canceled { info!("canceled request for {}", uri_str); return std::ptr::null_mut(); } - let new_buffer: JByteArray = env.get_field(&native_request_data_object, "buffer", "[B").expect("Failed to get buffer field").l().unwrap().into(); - let new_buffer_length = env.get_array_length(&new_buffer).expect("Failed to get array length") as usize; + let new_buffer: JByteArray = env + .get_field(&native_request_data_object, "buffer", "[B") + .expect("Failed to get buffer field") + .l() + .unwrap() + .into(); + let new_buffer_length = env + .get_array_length(&new_buffer) + .expect("Failed to get array length") as usize; let mut new_buffer_data = Box::new(vec![0i8; new_buffer_length]); - env.get_byte_array_region(&new_buffer, 0, new_buffer_data.as_mut_slice()).expect("Failed to get byte array region"); + env.get_byte_array_region(&new_buffer, 0, new_buffer_data.as_mut_slice()) + .expect("Failed to get byte array region"); - let ref_counter_struct_size = (slice_buffer.data as usize) - (slice_buffer.ref_counter as usize); + let ref_counter_struct_size = + (slice_buffer.data as usize) - (slice_buffer.ref_counter as usize); //we need to allocate a new ref_counter struct and copy the old ref_counter and the new_buffer to it let new_ref = { let new_ref = libc::malloc(ref_counter_struct_size + new_buffer_length) as *mut c_void; libc::memcpy(new_ref, slice_buffer.ref_counter, ref_counter_struct_size); - libc::memcpy(new_ref.offset(ref_counter_struct_size as isize), new_buffer_data.as_ptr() as *const c_void, new_buffer_length); + libc::memcpy( + new_ref.offset(ref_counter_struct_size as isize), + new_buffer_data.as_ptr() as *const c_void, + new_buffer_length, + ); libc::free(slice_buffer.ref_counter); new_ref }; @@ -104,20 +143,25 @@ def_hook!( pub fn init() { if let Some(signature) = sig::find_signature( &common::CLIENT_MODULE, - "A8 03 1F F8 ?? ?? 00 94 ?? ?? ?? 91 ?? ?? ?? A9", -0x48, - "0A 90 00 F0 3F F9", -0x37 + "AA A8 03 1F F8 ?? ?? 00 94 ?? ?? 05 91", + -0x47, + "0A 90 00 F0 3F F9", + -0x37, ) { dobby_hook!(signature as *mut c_void, unary_call); common::attach_jni_env(|env| { - NATIVE_LIB_ON_UNARY_CALL_METHOD.set( - env.get_method_id( - env.get_object_class(common::native_lib_instance()).unwrap(), - "onNativeUnaryCall", - "(Ljava/lang/String;[B)Lme/eternal/purrfectsnap/nativelib/NativeRequestData;" - ).expect("Failed to get onNativeUnaryCall method id") - ).expect("unary call method already set"); + NATIVE_LIB_ON_UNARY_CALL_METHOD + .set( + env.get_method_id( + env.get_object_class(common::native_lib_instance()).unwrap(), + "onNativeUnaryCall", + "(Ljava/lang/String;[B)Lme/eternal/purrfectsnap/nativelib/NativeRequestData;", + ) + .expect("Failed to get onNativeUnaryCall method id"), + ) + .expect("unary call method already set"); }); } else { - error!("Can't find unaryCall signature"); + panic!("Can't find unaryCall signature"); } } diff --git a/native/rust/src/modules/util/elf.rs b/native/rust/src/modules/util/elf.rs new file mode 100644 index 00000000..7c34ec77 --- /dev/null +++ b/native/rust/src/modules/util/elf.rs @@ -0,0 +1,61 @@ +use procfs::process::MMapPath; + +pub struct Elf<'elf> { + base_address: usize, + elf: goblin::elf::Elf<'elf>, + _buffer: &'elf [u8], +} + +impl<'elf> Elf<'elf> { + pub fn from_maps(lib: &str) -> Option { + let maps = procfs::process::Process::myself().ok()?.maps().ok()?; + + for memory_map in maps.iter() { + if let MMapPath::Path(path) = &memory_map.pathname { + let path = path.to_string_lossy(); + + if !path.contains(lib) { + continue; + } + + let file_data = std::fs::read(path.to_string()).ok()?; + let file_buffer = Box::leak(file_data.into_boxed_slice()); + + if let Ok(elf) = goblin::elf::Elf::parse(file_buffer) { + return Some(Elf { + base_address: memory_map.address.0 as usize, + elf, + _buffer: file_buffer, + }); + } else { + warn!( + "Failed to parse ELF for library {} at address {:x}", + lib, memory_map.address.0 + ); + } + } + } + + None + } + + pub fn get_symbol_address(&self, symbol: &str) -> Option { + for sym in &self.elf.dynsyms { + if let Some(name) = self.elf.dynstrtab.get_at(sym.st_name) { + if name == symbol { + return Some(self.base_address + sym.st_value as usize); + } + } + } + + for sym in &self.elf.syms { + if let Some(name) = self.elf.strtab.get_at(sym.st_name) { + if name == symbol { + return Some(self.base_address + sym.st_value as usize); + } + } + } + + None + } +} diff --git a/native/rust/src/modules/util/mod.rs b/native/rust/src/modules/util/mod.rs index 898e19a5..7c18c1f7 100644 --- a/native/rust/src/modules/util/mod.rs +++ b/native/rust/src/modules/util/mod.rs @@ -1 +1,2 @@ +pub mod elf; pub mod valdi_utils; diff --git a/native/rust/src/modules/util/valdi_utils.rs b/native/rust/src/modules/util/valdi_utils.rs index 6dc4cd31..a55c51a5 100644 --- a/native/rust/src/modules/util/valdi_utils.rs +++ b/native/rust/src/modules/util/valdi_utils.rs @@ -43,7 +43,12 @@ pub struct ValdiModule { impl ValdiModule { pub fn parse(buffer: Vec) -> Result { let mut offset = 0; - let magic = u32::from_be_bytes([buffer[offset], buffer[offset + 1], buffer[offset + 2], buffer[offset + 3]]); + let magic = u32::from_be_bytes([ + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + ]); offset += 4; @@ -85,13 +90,12 @@ impl ValdiModule { tags.push(ModuleTag::new(has_padding, tag_buffer)); } - let tags = tags.chunks(2).map(|chunk| { - (chunk[0].clone(), chunk[1].clone()) - }).collect(); + let tags = tags + .chunks(2) + .map(|chunk| (chunk[0].clone(), chunk[1].clone())) + .collect(); - Ok(ValdiModule { - tags, - }) + Ok(ValdiModule { tags }) } pub fn to_bytes(&self) -> Vec { diff --git a/native/rust/src/modules/valdi_hook.rs b/native/rust/src/modules/valdi_hook.rs index d910ecdd..6047a430 100644 --- a/native/rust/src/modules/valdi_hook.rs +++ b/native/rust/src/modules/valdi_hook.rs @@ -1,35 +1,29 @@ -#![allow(dead_code, unused_imports)] - -use super::util::valdi_utils::{ValdiModule, ModuleTag}; -use std::{collections::HashMap, ffi::c_void, sync::Mutex}; +use super::util::valdi_utils::{ModuleTag, ValdiModule}; +use crate::{config, def_hook, dobby_hook, modules::util::elf, util::get_jni_string}; use jni::{objects::JString, JNIEnv}; use once_cell::sync::Lazy; -use crate::{common, config, def_hook, dobby_hook, dobby_hook_sym, util::get_jni_string}; +use std::{ + collections::HashMap, + ffi::c_void, + sync::Mutex, +}; static AASSET_MAP: Lazy>>> = Lazy::new(|| Mutex::new(HashMap::new())); static LOADER_DATA: Mutex> = Mutex::new(None); -def_hook!( - aasset_get_length, - i32, - |arg0: *mut c_void| { - if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) { - return buffer.len() as i32; - } - aasset_get_length_original.unwrap()(arg0) +def_hook!(aasset_get_length, i32, |arg0: *mut c_void| { + if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) { + return buffer.len() as i32; } -); + aasset_get_length_original.unwrap()(arg0) +}); -def_hook!( - aasset_get_buffer, - *const c_void, - |arg0: *mut c_void| { - if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) { - return buffer.as_ptr() as *const c_void; - } - aasset_get_buffer_original.unwrap()(arg0) +def_hook!(aasset_get_buffer, *const c_void, |arg0: *mut c_void| { + if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) { + return buffer.as_ptr() as *const c_void; } -); + aasset_get_buffer_original.unwrap()(arg0) +}); def_hook!( aasset_manager_open, @@ -45,9 +39,13 @@ def_hook!( let loader_data = LOADER_DATA.lock().unwrap().clone().expect("No loader data"); - let archive_buffer: Vec = std::slice::from_raw_parts(asset_buffer as *const u8, asset_length as usize).to_vec(); - let decompressed = zstd::stream::decode_all(&archive_buffer[..]).expect("Failed to decompress valdi archive"); - let mut valdi_module = ValdiModule::parse(decompressed).expect("Failed to parse valdi module"); + let archive_buffer: Vec = + std::slice::from_raw_parts(asset_buffer as *const u8, asset_length as usize) + .to_vec(); + let decompressed = zstd::stream::decode_all(&archive_buffer[..]) + .expect("Failed to decompress valdi archive"); + let mut valdi_module = + ValdiModule::parse(decompressed).expect("Failed to parse valdi module"); let mut tags = valdi_module.get_tags(); let mut new_tags = Vec::new(); @@ -58,19 +56,22 @@ def_hook!( continue; } - let old_file_name = name.split_once(".").unwrap().0.to_owned() + rand::random::().to_string().as_str(); + let old_file_name = name.split_once(".").unwrap().0.to_owned() + + rand::random::().to_string().as_str(); tag1.set_buffer((old_file_name.to_owned() + ".js").as_bytes().to_vec()); - let original_module_path = path.split_once(".").unwrap().0.to_owned() + "/" + &old_file_name; + let original_module_path = + path.split_once(".").unwrap().0.to_owned() + "/" + &old_file_name; - let hooked_module = format!("{};module.exports = require(\"{}\");", loader_data, original_module_path); - - new_tags.push( - ( - ModuleTag::new(true, name.as_bytes().to_vec()), - ModuleTag::new(true, hooked_module.as_bytes().to_vec()) - ) + let hooked_module = format!( + "{};module.exports = require(\"{}\");", + loader_data, original_module_path ); + new_tags.push(( + ModuleTag::new(true, name.as_bytes().to_vec()), + ModuleTag::new(true, hooked_module.as_bytes().to_vec()), + )); + debug!("Valdi loader injected in {}", name); break; } @@ -79,22 +80,22 @@ def_hook!( valdi_module.set_tags(tags); let compressed = valdi_module.to_bytes(); - let compressed = zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress"); + let compressed = + zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress"); - AASSET_MAP.lock().unwrap().insert(handle as usize, compressed); + AASSET_MAP + .lock() + .unwrap() + .insert(handle as usize, compressed); } handle } ); -def_hook!( - aasset_close, - c_void, - |handle: *mut c_void| { - AASSET_MAP.lock().unwrap().remove(&(handle as usize)); - aasset_close_original.unwrap()(handle) - } -); +def_hook!(aasset_close, c_void, |handle: *mut c_void| { + AASSET_MAP.lock().unwrap().remove(&(handle as usize)); + aasset_close_original.unwrap()(handle) +}); pub fn set_valdi_loader(mut env: JNIEnv, _: *mut c_void, code: JString) { let new_code = get_jni_string(&mut env, code).expect("Failed to get loader code"); @@ -103,11 +104,33 @@ pub fn set_valdi_loader(mut env: JNIEnv, _: *mut c_void, code: JString) { pub fn init() { if !config::native_config().valdi_hooks { - return + return; } - dobby_hook_sym!("libandroid.so", "AAsset_getBuffer", aasset_get_buffer); - dobby_hook_sym!("libandroid.so", "AAsset_getLength", aasset_get_length); - dobby_hook_sym!("libandroid.so", "AAsset_close", aasset_close); - dobby_hook_sym!("libandroid.so", "AAssetManager_open", aasset_manager_open); + let lib_android = + elf::Elf::from_maps("/libandroid.so").expect("Failed to find libandroid.so in maps"); + + if let Some(ptr) = lib_android.get_symbol_address("AAsset_getBuffer") { + dobby_hook!(ptr as _, aasset_get_buffer); + } else { + panic!("Failed to find AAsset_getBuffer symbol"); + } + + if let Some(ptr) = lib_android.get_symbol_address("AAsset_getLength") { + dobby_hook!(ptr as _, aasset_get_length); + } else { + panic!("Failed to find AAsset_getLength symbol"); + } + + if let Some(ptr) = lib_android.get_symbol_address("AAsset_close") { + dobby_hook!(ptr as _, aasset_close); + } else { + panic!("Failed to find AAsset_close symbol"); + } + + if let Some(ptr) = lib_android.get_symbol_address("AAssetManager_open") { + dobby_hook!(ptr as _, aasset_manager_open); + } else { + panic!("Failed to find AAssetManager_open symbol"); + } } diff --git a/native/rust/src/sig.rs b/native/rust/src/sig.rs index e86c86f8..4ed011d6 100644 --- a/native/rust/src/sig.rs +++ b/native/rust/src/sig.rs @@ -61,8 +61,18 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o let mut mask = Vec::new(); let mut i = 0; - if let Some(cache) = SIGNATURE_CACHE.lock().unwrap().iter().find(|(sig, _)| sig == pattern) { - return cache.1.clone().into_iter().map(|offset| module_base + offset).collect(); + if let Some(cache) = SIGNATURE_CACHE + .lock() + .unwrap() + .iter() + .find(|(sig, _)| sig == pattern) + { + return cache + .1 + .clone() + .into_iter() + .map(|offset| module_base + offset) + .collect(); } while i < pattern.len() { @@ -70,7 +80,7 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o bytes.push(0); mask.push('?'); } else { - bytes.push(u8::from_str_radix(&pattern[i..i+2], 16).unwrap()); + bytes.push(u8::from_str_radix(&pattern[i..i + 2], 16).unwrap()); mask.push('x'); } i += 3; @@ -92,7 +102,10 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o } if found { if once { - SIGNATURE_CACHE.lock().unwrap().push((pattern.to_string(), vec![i])); + SIGNATURE_CACHE + .lock() + .unwrap() + .push((pattern.to_string(), vec![i])); return vec![module_base + i]; } results.push(module_base + i); @@ -100,14 +113,22 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o i += 1; } - SIGNATURE_CACHE.lock().unwrap().push((pattern.to_string(), results.clone())); + SIGNATURE_CACHE + .lock() + .unwrap() + .push((pattern.to_string(), results.clone())); results } pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Option { - let executable_regions = mapped_lib.regions.iter().filter(|region| { - region.perms.contains(MMPermissions::EXECUTE) - }).collect::>(); + let executable_regions = mapped_lib + .regions + .iter() + .filter(|region| { + region.perms.contains(MMPermissions::EXECUTE) + && region.perms.contains(MMPermissions::READ) + }) + .collect::>(); for region in executable_regions { let size = (region.end - region.start) as usize; @@ -117,16 +138,27 @@ pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Optio let bytes_buffer = match read_region_bytes(module_base, size) { Some(buffer) => buffer, None => { - warn!("Unable to read executable region: {:#x} - {:#x}", region.start, region.end); + warn!( + "Unable to read executable region: {:#x} - {:#x}", + region.start, region.end + ); continue; } }; let results = find_signatures(module_base, &bytes_buffer, pattern, true); if results.is_empty() { - warn!("Signature not found in region: {:#x} - {:#x}", region.start, region.end); + warn!( + "Signature not found in region: {:#x} - {:#x}", + region.start, region.end + ); } else { - debug!("Found {} results in region: {:#x} - {:#x}", results.len(), region.start, region.end); + debug!( + "Found {} results in region: {:#x} - {:#x}", + results.len(), + region.start, + region.end + ); return Some(results[0]); } } @@ -135,13 +167,21 @@ pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Optio None } -pub fn find_signature(mapped_lib: &MappedLib, _arm64_pattern: &str, _arm64_offset: i64, _arm32_pattern: &str, _arm32_offset: i64) -> Option { +pub fn find_signature( + mapped_lib: &MappedLib, + _arm64_pattern: &str, + _arm64_offset: i64, + _arm32_pattern: &str, + _arm32_offset: i64, +) -> Option { #[cfg(target_arch = "aarch64")] { - return find_signature_executable(mapped_lib, _arm64_pattern).map(|address| (address as i64 + _arm64_offset) as usize); + return find_signature_executable(mapped_lib, _arm64_pattern) + .map(|address| (address as i64 + _arm64_offset) as usize); } #[cfg(target_arch = "arm")] { - return find_signature_executable(mapped_lib, _arm32_pattern).map(|address| (address as i64 + _arm32_offset) as usize); + return find_signature_executable(mapped_lib, _arm32_pattern) + .map(|address| (address as i64 + _arm32_offset) as usize); } }