Decode avatar manifests and paginated catalogs on Android
This commit is contained in:
1 parent
c4c11ad813
commit
07aa436380
3 files changed
+35
-7
No files matched your search
@@ -7,8 +7,12 @@ import java.nio.ByteOrder
|
||||
object AvatarProtocol {
|
||||
const val MAX_USER = 1024 * 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 validObject(file: String): Boolean = objectPattern.matches(file)
|
||||
// channel_id в БД/событиях — десятичный u64; avatar_server в userinfo — hex.
|
||||
fun groupId(id: String): Long = id.toULongOrNull(10)?.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,
|
||||
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 {
|
||||
require(data.size >= 141) { "file response length=${data.size}" }
|
||||
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 id = b.long; val owner = 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 + 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)
|
||||
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) {
|
||||
"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? {
|
||||
require(data.size == 51) { "transfer length=${data.size}" }
|
||||
|
||||
@@ -163,7 +163,7 @@ fun parseConnList(data: ByteArray): List<ConnInfo> {
|
||||
}
|
||||
|
||||
/* Размер общего снимка из chat_member.h; одинаковый для desktop и Android. */
|
||||
const val MEMBER_WIRE_SIZE = 157
|
||||
const val MEMBER_WIRE_SIZE = 158
|
||||
data class MemberWire(
|
||||
val nodeId: Long, val online: Int, val isSelf: Boolean,
|
||||
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 name = String(nameBytes).trimEnd('\u0000')
|
||||
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 storageRole = buf.get().toInt() != 0
|
||||
return MemberWire(nodeId, online, isSelf, flags, rtt, natType, storage, name, avatarServer, avatarFile, storageRole)
|
||||
|
||||
@@ -119,8 +119,12 @@ object NativeLib {
|
||||
fun requestMemberList(channelId: String): Boolean = nativeRequestMemberList(channelId)
|
||||
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,
|
||||
upload: Boolean = false): Boolean =
|
||||
nativeAvatarOperation(if (upload) 2 else 1, id, group, server, owner, remote, local, null)
|
||||
upload: Boolean = false, maxBytes: Int = AvatarProtocol.MAX_ORIGINAL): Boolean =
|
||||
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 =
|
||||
nativeAvatarOperation(3, id, group, server, owner, "avatars", null, null)
|
||||
fun avatarStat(id: Long, group: Long, server: Long, owner: Long, remote: String): Boolean =
|
||||
|
||||
Reference in new issue
Block a user