From 4fbb825b9b5b719eeadb34f519e711cb6e1a204f Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 09:28:05 +0100 Subject: [PATCH] Reject partial Android repository snapshots and distinguish query failures --- .../java/com/utun/chat/data/ChatRepository.kt | 64 ++++++++++++------- .../main/java/com/utun/chat/data/NativeLib.kt | 2 +- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index 5cd40ca6..f2edaad9 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -1,7 +1,6 @@ package com.utun.chat.data import com.utun.chat.R -import android.util.Log import org.json.JSONArray import org.json.JSONObject import java.nio.ByteBuffer @@ -96,7 +95,9 @@ data class LinkStatus(val linkId: Int = 0, val localFamily: Int = 0, val localAd val linkState: Int = 0, val linkStatus: Int = 0, val isServer: Boolean = false, val socketName: String = "", val protocol: String = "") { val stateLabel: String get() = AppLanguage.text(when (linkState) { - 2 -> R.string.connection_up; 1 -> R.string.connection_connecting; else -> R.string.connection_down + 3 -> if (linkStatus == 1) R.string.connection_up else R.string.connection_down + 1, 2 -> R.string.connection_connecting + else -> R.string.connection_down }) } @@ -128,19 +129,23 @@ data class ConnInfo( /* Format: [count:2][entry:46B]* */ fun parseConnList(data: ByteArray): List { - if (data.size < 2) return emptyList() + if (data.size < 2) { + LogManager.addLog("ERROR", "Repository", "Connection snapshot header truncated bytes=${data.size}"); return emptyList() + } val buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN) val count = buf.getShort().toInt() and 0xFFFF + if (data.size != 2 + count * 46) { + LogManager.addLog("ERROR", "Repository", "Connection snapshot truncated count=$count bytes=${data.size}"); return emptyList() + } val list = mutableListOf() for (i in 0 until count) { - if (buf.position() + 46 > data.size) break val nodeId = buf.getLong() val flags = buf.get().toInt() and 0xFF val linkCount = buf.get().toInt() and 0xFF val rtt = buf.getShort().toInt() and 0xFFFF buf.getShort() val nameBytes = ByteArray(32); buf.get(nameBytes) - val name = String(nameBytes).trimEnd('\u0000').trimEnd() + val name = String(nameBytes, Charsets.UTF_8).substringBefore('\u0000').trimEnd() if (nodeId == 0L) continue list.add(ConnInfo( nodeId = nodeId, name = name.ifEmpty { "%016X".format(nodeId) }, @@ -208,8 +213,9 @@ fun MemberWire.toChatMember(previous: ChatMember = ChatMember(nodeId, name)): Ch fun cleanSocketName(raw: String): String = raw.removePrefix("as_").replace(Regex("_v[46]_(tcp|udp)$"), " $1") +/** Stateless JNI/JSON adapter. Снимки применяются целиком: null означает отказ, пустой список — корректный пустой снимок. */ class ChatRepository { - fun getChannels(): List { + fun getChannels(): List? { val list = mutableListOf() try { val arr = JSONArray(NativeLib.getChannels()) @@ -224,7 +230,9 @@ class ChatRepository { lastMsgEncrypted = obj.optBoolean("lastMsgEncrypted") )) } - } catch (e: Exception) { Log.w("utun-gui", "getChannels error", e) } + } catch (e: Exception) { + LogManager.addLog("ERROR", "Repository", "Channel snapshot failed: $e"); return null + } return list } @@ -235,11 +243,11 @@ class ChatRepository { val obj = arr.getJSONObject(i) if (obj.getString("id") == channelId) return obj.optInt("peersOnline", 0) } - } catch (e: Exception) { Log.w("utun-gui", "getPeersOnline error", e) } + } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Peer count failed channel=$channelId: $e") } return 0 } - fun getMessages(channelId: String, limit: Int = 50, anchor: String = "", exact: Boolean = false): List { + fun getMessages(channelId: String, limit: Int = 50, anchor: String = "", exact: Boolean = false): List? { val list = mutableListOf() try { val arr = JSONArray(NativeLib.getMessages(channelId, limit, anchor, exact)) @@ -292,7 +300,7 @@ class ChatRepository { val durPart = text.substring(wfEnd + 5).takeWhile { it != ';' } voiceDurationMs = durPart.toIntOrNull() ?: 0 } - } catch (_: Exception) {} + } catch (e: Exception) { LogManager.addLog("WARN", "Repository", "Voice metadata failed channel=$channelId: $e") } } if (!encrypted && (contentType == "application/octet-stream" || contentType.startsWith("video/") || contentType.startsWith("image/"))) { @@ -304,7 +312,7 @@ class ChatRepository { if (metaParts.isNotEmpty()) fileSize = metaParts[0].toLongOrNull() ?: 0L } - } catch (_: Exception) {} + } catch (e: Exception) { LogManager.addLog("WARN", "Repository", "File metadata failed channel=$channelId: $e") } } var imageWidth = obj.optInt("width") @@ -317,7 +325,7 @@ class ChatRepository { part.startsWith("h=") -> imageHeight = part.removePrefix("h=").toIntOrNull() ?: 0 } } - } catch (_: Exception) {} + } catch (e: Exception) { LogManager.addLog("WARN", "Repository", "Image metadata failed channel=$channelId: $e") } } var videoWidth = obj.optInt("width") @@ -332,7 +340,7 @@ class ChatRepository { part.startsWith("dur=") -> videoDurationMs = part.removePrefix("dur=").toIntOrNull() ?: 0 } } - } catch (_: Exception) {} + } catch (e: Exception) { LogManager.addLog("WARN", "Repository", "Video metadata failed channel=$channelId: $e") } } list.add(Message( @@ -360,7 +368,9 @@ class ChatRepository { deliveryStatusRes = if (encryptionState == "ready") R.string.attachment_encrypted else 0 )) } - } catch (e: Exception) { Log.w("utun-gui", "getMessages error", e) } + } catch (e: Exception) { + LogManager.addLog("ERROR", "Repository", "Message snapshot failed channel=$channelId anchor=$anchor: $e"); return null + } return list } @@ -408,7 +418,9 @@ class ChatRepository { storageRole = obj.optInt("sr", 0) != 0 )) } - } catch (e: Exception) { Log.w("utun-gui", "getMembers error", e) } + } catch (e: Exception) { + LogManager.addLog("ERROR", "Repository", "Member snapshot failed channel=$channelId: $e"); return emptyList() + } return list } @@ -445,7 +457,7 @@ class ChatRepository { inviterId = if (obj.isNull("inviter_id")) null else obj.getLong("inviter_id"), inviterName = obj.optString("inviter_name", "") ) - } catch (e: Exception) { Log.w("utun-gui", "getMemberDetail error", e) } + } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Member detail failed channel=$channelId node=$nodeId: $e") } return null } @@ -493,11 +505,11 @@ class ChatRepository { linksUp = obj.optInt("lu", 0), links = links ) - } catch (e: Exception) { Log.w("utun-gui", "getMemberLinks error", e) } + } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Member links failed node=$nodeId: $e") } return MemberLinks() } - fun parseLocalSockets(json: String): List { + fun parseLocalSockets(json: String): List? { try { val arr = JSONArray(json) val list = mutableListOf() @@ -523,8 +535,8 @@ class ChatRepository { } list.sortWith(compareBy { it.ifName }.thenBy { it.bindIp }.thenBy { it.protocol }.thenBy { it.port }) return list - } catch (e: Exception) { Log.w("utun-gui", "local sockets snapshot parse error", e) } - return emptyList() + } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Socket snapshot parse failed: $e") } + return null } fun close() {} @@ -537,7 +549,7 @@ class ChatRepository { fun dmSend(convId: String, text: String, generation: Long, requestId: Long): Boolean = NativeLib.dmSend(convId, text, generation, requestId) - fun parseDmConversations(json: String): List { + fun parseDmConversations(json: String): List? { val list = mutableListOf() try { val arr = JSONArray(json) @@ -550,11 +562,13 @@ class ChatRepository { lastTs = obj.optLong("last_ts", 0), generation = obj.getLong("generation") )) } - } catch (e: Exception) { Log.w("utun-gui", "getDmConversations error", e) } + } catch (e: Exception) { + LogManager.addLog("ERROR", "Repository", "PM conversation snapshot failed: $e"); return null + } return list } - fun parseDmMessages(json: String, peerName: String = ""): List { + fun parseDmMessages(json: String, peerName: String = ""): List? { val list = mutableListOf() try { val arr = JSONArray(json) @@ -597,7 +611,9 @@ class ChatRepository { deliveryStatusRes = delivery )) } - } catch (e: Exception) { Log.w("utun-gui", "getDmMessages error", e) } + } catch (e: Exception) { + LogManager.addLog("ERROR", "Repository", "PM message snapshot failed: $e"); return null + } return list } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 0216464d..15a1fa6e 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -124,7 +124,7 @@ object NativeLib { fun deriveNodeId(pubkey: ByteArray): Long = nativeDeriveNodeId(pubkey) fun connectChannel(channelId: String) { nativeConnectChannel(channelId) } fun connectMemberNode(channelId: String, nodeId: Long) { nativeConnectMemberNode(channelId, nodeId) } - fun getChannels(): String = nativeGetChannels() ?: "[]" + fun getChannels(): String = checkNotNull(nativeGetChannels()) { "Channel snapshot unavailable" } fun getMessages(channelId: String, limit: Int = 50, anchor: String = "", exact: Boolean = false): String = nativeGetMessages(channelId, limit, messageIdBytes(anchor), exact) ?: "[]" fun feedbackCommand(json: String, requestId: Long = 0): Boolean = nativeFeedbackCommand(json.toByteArray(Charsets.UTF_8), requestId)