Browse Source

Decode avatar manifests and paginated catalogs on Android

master
evgeny 6 days ago
parent
commit
07aa436380
  1. 30
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarProtocol.kt
  2. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  3. 8
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

30
tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarProtocol.kt

@ -7,8 +7,12 @@ import java.nio.ByteOrder
object AvatarProtocol { object AvatarProtocol {
const val MAX_USER = 1024 * 1024 const val MAX_USER = 1024 * 1024
const val MAX_GROUP = 16 * 1024 const val MAX_GROUP = 16 * 1024
private val filePattern = Regex("[0-9a-f]{64}\\.png") const val MAX_ORIGINAL = 32 * 1024 * 1024
const val MAX_MANIFEST = 4096
private val filePattern = Regex("[0-9a-f]{64}\\.json")
private val objectPattern = Regex("[0-9a-f]{64}\\.(png|jpg|webp|json)")
fun validFile(file: String): Boolean = filePattern.matches(file) fun validFile(file: String): Boolean = filePattern.matches(file)
fun validObject(file: String): Boolean = objectPattern.matches(file)
// channel_id в БД/событиях — десятичный u64; avatar_server в userinfo — hex. // channel_id в БД/событиях — десятичный u64; avatar_server в userinfo — hex.
fun groupId(id: String): Long = id.toULongOrNull(10)?.toLong() ?: 0 fun groupId(id: String): Long = id.toULongOrNull(10)?.toLong() ?: 0
fun node(id: String): Long = id.toULongOrNull(16)?.toLong() ?: 0 fun node(id: String): Long = id.toULongOrNull(16)?.toLong() ?: 0
@ -30,7 +34,9 @@ object AvatarProtocol {
} }
data class Reply(val id: Long, val group: Long, val server: Long = 0, val owner: Long = 0, data class Reply(val id: Long, val group: Long, val server: Long = 0, val owner: Long = 0,
val command: Int = 0, val status: Int = 0, val size: Long = 0, val hash: String = "", val fileType: Int = 0) val command: Int = 0, val status: Int = 0, val size: Long = 0, val hash: String = "", val fileType: Int = 0,
val cursor: Long = 0, val version: ByteArray = byteArrayOf(), val more: Boolean = false,
val path: String = "", val payload: ByteArray = byteArrayOf())
fun response(data: ByteArray): Reply { fun response(data: ByteArray): Reply {
require(data.size >= 141) { "file response length=${data.size}" } require(data.size >= 141) { "file response length=${data.size}" }
val b = buffer(data) val b = buffer(data)
@ -39,14 +45,32 @@ object AvatarProtocol {
val command = b.get().toInt() and 255; val status = b.short.toInt() and 65535 val command = b.get().toInt() and 255; val status = b.short.toInt() and 65535
val id = b.long; val owner = b.long val id = b.long; val owner = b.long
b.long; val size = b.long b.long; val size = b.long
val cursor = b.long; val version = ByteArray(16).also { b.get(it) }
b.position(16 + 77); val hash = ByteArray(32); b.get(hash) b.position(16 + 77); val hash = ByteArray(32); b.get(hash)
b.position(16 + 117); val fileType = b.get().toInt() and 255 b.position(16 + 117); val fileType = b.get().toInt() and 255
val more = b.get().toInt() and 255; require(more in 0..1) { "invalid list more flag" }
b.position(16 + 119) b.position(16 + 119)
val path = b.short.toInt() and 65535; val target = b.short.toInt() and 65535; val payload = b.short.toInt() and 65535 val path = b.short.toInt() and 65535; val target = b.short.toInt() and 65535; val payload = b.short.toInt() and 65535
require(path <= 1024 && target <= 1024 && payload <= 8192 && b.remaining() == path + target + payload) { require(path <= 1024 && target <= 1024 && payload <= 8192 && b.remaining() == path + target + payload) {
"invalid file response body lengths" "invalid file response body lengths"
} }
return Reply(id, group, server, owner, command, status, size, hash(hash), fileType) val remote = ByteArray(path).also { b.get(it) }; b.position(b.position() + target)
val body = ByteArray(payload).also { b.get(it) }
return Reply(id, group, server, owner, command, status, size, hash(hash), fileType, cursor, version, more,
String(remote, Charsets.UTF_8), body)
}
data class Entry(val file: String, val mtime: Long)
fun entries(reply: Reply): List<Entry> {
val b = buffer(reply.payload); val result = mutableListOf<Entry>()
while (b.hasRemaining()) {
require(b.remaining() >= 35) { "truncated list entry" }
val length = b.short.toInt() and 65535; val type = b.get().toInt() and 255
val size = b.long; val mtime = b.long; b.position(b.position() + 16)
require(length in 1..255 && length <= b.remaining() && type in 1..2) { "invalid list entry" }
val name = String(ByteArray(length).also { b.get(it) }, Charsets.UTF_8)
if (type == 1 && validFile(name) && size in 1..MAX_MANIFEST) result += Entry(name, mtime)
}
return result
} }
fun transfer(data: ByteArray): Reply? { fun transfer(data: ByteArray): Reply? {
require(data.size == 51) { "transfer length=${data.size}" } require(data.size == 51) { "transfer length=${data.size}" }

4
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt

@ -163,7 +163,7 @@ fun parseConnList(data: ByteArray): List<ConnInfo> {
} }
/* Размер общего снимка из chat_member.h; одинаковый для desktop и Android. */ /* Размер общего снимка из chat_member.h; одинаковый для desktop и Android. */
const val MEMBER_WIRE_SIZE = 157 const val MEMBER_WIRE_SIZE = 158
data class MemberWire( data class MemberWire(
val nodeId: Long, val online: Int, val isSelf: Boolean, val nodeId: Long, val online: Int, val isSelf: Boolean,
val flags: Int, val rtt: Int, val natType: Int, val flags: Int, val rtt: Int, val natType: Int,
@ -184,7 +184,7 @@ fun parseMemberWire(data: ByteArray, offset: Int = 0): MemberWire? {
val nameBytes = ByteArray(64); buf.get(nameBytes) val nameBytes = ByteArray(64); buf.get(nameBytes)
val name = String(nameBytes).trimEnd('\u0000') val name = String(nameBytes).trimEnd('\u0000')
val avatarServer = buf.getLong() val avatarServer = buf.getLong()
val avatarBytes = ByteArray(69); buf.get(avatarBytes) val avatarBytes = ByteArray(70); buf.get(avatarBytes)
val avatarFile = String(avatarBytes).trimEnd('\u0000') val avatarFile = String(avatarBytes).trimEnd('\u0000')
val storageRole = buf.get().toInt() != 0 val storageRole = buf.get().toInt() != 0
return MemberWire(nodeId, online, isSelf, flags, rtt, natType, storage, name, avatarServer, avatarFile, storageRole) return MemberWire(nodeId, online, isSelf, flags, rtt, natType, storage, name, avatarServer, avatarFile, storageRole)

8
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -119,8 +119,12 @@ object NativeLib {
fun requestMemberList(channelId: String): Boolean = nativeRequestMemberList(channelId) fun requestMemberList(channelId: String): Boolean = nativeRequestMemberList(channelId)
fun requestGroupAvatar(id: Long, group: Long): Boolean = nativeAvatarOperation(0, id, group, 0, 0, null, null, null) fun requestGroupAvatar(id: Long, group: Long): Boolean = nativeAvatarOperation(0, id, group, 0, 0, null, null, null)
fun transferAvatar(id: Long, group: Long, server: Long, owner: Long, remote: String, local: String, fun transferAvatar(id: Long, group: Long, server: Long, owner: Long, remote: String, local: String,
upload: Boolean = false): Boolean = upload: Boolean = false, maxBytes: Int = AvatarProtocol.MAX_ORIGINAL): Boolean =
nativeAvatarOperation(if (upload) 2 else 1, id, group, server, owner, remote, local, null) nativeAvatarOperation(if (upload) 2 else 1, id, group, server, owner, remote, local,
java.nio.ByteBuffer.allocate(8).order(java.nio.ByteOrder.LITTLE_ENDIAN).putLong(maxBytes.toLong()).array())
fun avatarList(id: Long, group: Long, server: Long, owner: Long, cursor: Long, version: ByteArray): Boolean =
nativeAvatarOperation(8, id, group, server, owner, "avatars", null,
java.nio.ByteBuffer.allocate(24).order(java.nio.ByteOrder.LITTLE_ENDIAN).putLong(cursor).put(version).array())
fun avatarMkdir(id: Long, group: Long, server: Long, owner: Long): Boolean = fun avatarMkdir(id: Long, group: Long, server: Long, owner: Long): Boolean =
nativeAvatarOperation(3, id, group, server, owner, "avatars", null, null) nativeAvatarOperation(3, id, group, server, owner, "avatars", null, null)
fun avatarStat(id: Long, group: Long, server: Long, owner: Long, remote: String): Boolean = fun avatarStat(id: Long, group: Long, server: Long, owner: Long, remote: String): Boolean =

Loading…
Cancel
Save