Reject partial Android repository snapshots and distinguish query failures

This commit is contained in:
evgeny committed 2026-10-29 13:34:26 +01:00
1 parent e942190b9f
commit 4fbb825b9b
2 files changed
+41 -25

No files matched your search

@@ -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<ConnInfo> {
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<ConnInfo>()
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<Channel> {
fun getChannels(): List<Channel>? {
val list = mutableListOf<Channel>()
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<Message> {
fun getMessages(channelId: String, limit: Int = 50, anchor: String = "", exact: Boolean = false): List<Message>? {
val list = mutableListOf<Message>()
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<LocalSocketState> {
fun parseLocalSockets(json: String): List<LocalSocketState>? {
try {
val arr = JSONArray(json)
val list = mutableListOf<LocalSocketState>()
@@ -523,8 +535,8 @@ class ChatRepository {
}
list.sortWith(compareBy<LocalSocketState> { 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<DmConversation> {
fun parseDmConversations(json: String): List<DmConversation>? {
val list = mutableListOf<DmConversation>()
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<Message> {
fun parseDmMessages(json: String, peerName: String = ""): List<Message>? {
val list = mutableListOf<Message>()
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
}
}
@@ -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)