|
|
|
|
@ -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}" } |
|
|
|
|
|