summaryrefslogtreecommitdiff
path: root/adblib-tools/src/com/android/adblib/tools/debugging/packets/ddms/DdmsChunkUtils.kt
blob: cf4844123e658ab7739d78c87895f463568e3779 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.adblib.tools.debugging.packets.ddms

import com.android.adblib.AdbInputChannelSlice
import com.android.adblib.AdbOutputChannel
import com.android.adblib.ByteBufferAdbOutputChannel
import com.android.adblib.forwardTo
import com.android.adblib.skipRemaining
import com.android.adblib.tools.debugging.packets.AdbBufferedInputChannel
import com.android.adblib.tools.debugging.packets.JdwpPacketView
import com.android.adblib.tools.debugging.packets.copy
import com.android.adblib.tools.debugging.packets.ddms.DdmsPacketConstants.DDMS_CHUNK_BYTE_ORDER
import com.android.adblib.tools.debugging.packets.ddms.DdmsPacketConstants.DDMS_CHUNK_HEADER_LENGTH
import com.android.adblib.tools.debugging.packets.ddms.DdmsPacketConstants.DDMS_CMD
import com.android.adblib.tools.debugging.packets.ddms.DdmsPacketConstants.DDMS_CMD_SET
import com.android.adblib.tools.debugging.toByteBuffer
import com.android.adblib.utils.ResizableBuffer
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.EOFException

/**
 * Serialize the [DdmsChunkView] into an [AdbOutputChannel].
 *
 * @throws IllegalArgumentException if [DdmsChunkView.payload] does not contain exactly
 * [DdmsChunkView.length] bytes
 *
 * @param workBuffer (Optional) The [ResizableBuffer] used to transfer data
 */
internal suspend fun DdmsChunkView.writeToChannel(
    channel: AdbOutputChannel,
    workBuffer: ResizableBuffer = ResizableBuffer()
) {
    workBuffer.clear()
    workBuffer.order(DDMS_CHUNK_BYTE_ORDER)

    // Write header
    workBuffer.appendInt(type.value)
    workBuffer.appendInt(length)
    channel.writeExactly(workBuffer.forChannelWrite())

    // Write payload
    val byteCount = payload.forwardTo(channel, workBuffer)
    payload.rewind()
    checkChunkLength(byteCount)
}

/**
 * Returns an in-memory copy of this [DdmsChunkView].
 *
 * @throws IllegalArgumentException if [DdmsChunkView.payload] does not contain exactly
 * [DdmsChunkView.length] bytes
 *
 * @param workBuffer (Optional) The [ResizableBuffer] used to transfer data
 */
internal suspend fun DdmsChunkView.clone(
    workBuffer: ResizableBuffer = ResizableBuffer()
): DdmsChunkView {
    val ddmsChunk = MutableDdmsChunk()

    // Copy header
    ddmsChunk.length = length
    ddmsChunk.type = type

    // Copy payload to "workBuffer"
    workBuffer.clear()
    val dataCopy = ByteBufferAdbOutputChannel(workBuffer)
    val byteCount = payload.forwardTo(dataCopy)
    payload.rewind()
    ddmsChunk.checkChunkLength(byteCount)

    // Make a copy into our own ByteBuffer
    val bufferCopy = workBuffer.forChannelWrite().copy()

    // Create rewindable channel for payload
    ddmsChunk.payload = AdbBufferedInputChannel.forByteBuffer(bufferCopy)

    return ddmsChunk
}

/**
 * Provides a view of a [JdwpPacketView] as a "DDMS" packet. A "DDMS" packet is a
 * "JDWP" packet that contains one or more [chunks][DdmsChunkView].
 *
 * Each chunk starts with an 8 bytes header followed by chunk specific payload
 *
 *  * chunk type (4 bytes)
 *  * chunk length (4 bytes)
 *  * chunk payload (variable, specific to the chunk type)
 */
internal fun JdwpPacketView.ddmsChunks(
    workBuffer: ResizableBuffer = ResizableBuffer()
): Flow<DdmsChunkView> {
    val jdwpPacketView = this
    return flow {
        if (!isReply && !isCommand(DDMS_CMD_SET, DDMS_CMD)) {
            throw IllegalArgumentException("JDWP packet is not a DDMS command packet (and is not a reply packet)")
        }

        jdwpPacketView.payload.rewind()
        workBuffer.clear()
        workBuffer.order(DDMS_CHUNK_BYTE_ORDER)
        while (true) {
            try {
                jdwpPacketView.payload.readExactly(workBuffer.forChannelRead(DDMS_CHUNK_HEADER_LENGTH))
            } catch (e: EOFException) {
                // Regular exit: there are no more chunks to be read
                break
            }
            val payloadBuffer = workBuffer.afterChannelRead()

            // Prepare chunk source
            val chunk = MutableDdmsChunk().apply {
                this.type = DdmsChunkTypes(payloadBuffer.getInt())
                this.length = payloadBuffer.getInt()
                val slice = AdbInputChannelSlice(jdwpPacketView.payload, this.length)
                this.payload = AdbBufferedInputChannel.forInputChannel(slice)
            }

            // Emit it to collector
            emit(chunk)

            // Ensure we consume all bytes from the chunk payload in case the collector did not
            // do anything with it
            chunk.payload.skipRemaining(workBuffer)
        }
    }
}

val JdwpPacketView.isDdmsCommand: Boolean
    get() = isCommand(DDMS_CMD_SET, DDMS_CMD)

private fun DdmsChunkView.checkChunkLength(byteCount: Int) {
    val expectedByteCount = length
    if (byteCount != expectedByteCount) {
        throw IllegalArgumentException(
            "DDMS packet should contain $expectedByteCount " +
                    "bytes but contains $byteCount bytes instead"
        )
    }
}

internal suspend fun DdmsChunkView.createFailException(): DdmsFailException {
    try {
        // See https://cs.android.com/android/platform/superproject/+/android13-release:libcore/dalvik/src/main/java/org/apache/harmony/dalvik/ddmc/ChunkHandler.java;l=102
        // 0-3: error code
        // 4-7: error message: UTF-16 character count
        // 8-n: error message: UTF-16 characters
        val buffer = payload.toByteBuffer(length).order(DDMS_CHUNK_BYTE_ORDER)
        val errorCode = buffer.getInt()
        val charCount = buffer.getInt()
        val data = CharArray(charCount)
        for (i in 0 until charCount) {
            data[i] = buffer.getChar()
        }
        val message = String(data)
        return DdmsFailException(errorCode, message)
    } catch (t: Throwable) {
        // In case the FAIL packet format is invalid, return a generic error, since this method is
        // called in the context of handling an error.
        return DdmsFailException(-1, "Unknown error due to invalid FAIL packet format").also {
            it.addSuppressed(t)
        }
    }
}

internal suspend fun DdmsChunkView.throwFailException(): Nothing {
    throw createFailException()
}

class DdmsFailException(val errorCode: Int, val failMessage: String) : Exception() {

    override val message: String?
        get() = "DDMS Failure on AndroidVM: errorCode=$errorCode, message=$failMessage"
}