diff --git a/src/main/kotlin/com/malinskiy/adam/request/sync/ListFilesRequest.kt b/src/main/kotlin/com/malinskiy/adam/request/sync/ListFilesRequest.kt new file mode 100644 index 000000000..7457ba821 --- /dev/null +++ b/src/main/kotlin/com/malinskiy/adam/request/sync/ListFilesRequest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 Anton Malinskiy + * + * 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.malinskiy.adam.request.sync + +import com.malinskiy.adam.Const + + +class ListFilesRequest(private val directory: String) : SyncShellCommandRequest>( + cmd = "ls -l $directory" +) { + private val builder = StringBuilder() + private val lslRegex: Regex = ("^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+" + //permissions + "(?:\\d+\\s+)?" + //nlink + "(\\S+)\\s+" + //user + "(\\S+)\\s+" + //group + "([\\d\\s,]*)\\s+" + //size + "(\\d{4}-\\d\\d-\\d\\d)\\s+" + //date + "(\\d\\d:\\d\\d)\\s+" + //time + "(.*)$").toRegex() // + + override suspend fun process(bytes: ByteArray, offset: Int, limit: Int) { + val part = String(bytes, 0, limit, Const.DEFAULT_TRANSPORT_ENCODING) + builder.append(part) + } + + override fun transform(): List { + return builder.lines() + .filter { it.isNotBlank() } + .mapNotNull { lslRegex.find(it) } + .map { match -> + val permissions = match.groupValues[1] + val owner = match.groupValues[2] + val group = match.groupValues[3] + val size = match.groupValues[4].toLongOrNull(10) ?: 0 + val date = match.groupValues[5] + val time = match.groupValues[6] + var name = match.groupValues[7] + + val type = when (permissions[0]) { + '-' -> AndroidFileType.REGULAR_FILE + 'b' -> AndroidFileType.BLOCK_SPECIAL_FILE + 'd' -> AndroidFileType.DIRECTORY + 'l' -> AndroidFileType.SYMBOLIC_LINK + 'c' -> AndroidFileType.CHARACTER_SPECIAL_FILE + 's' -> AndroidFileType.SOCKET_LINK + 'p' -> AndroidFileType.FIFO + else -> AndroidFileType.OTHER + } + + var link: String? = null + if (type == AndroidFileType.SYMBOLIC_LINK) { + val split = name.split("->") + if (split.size != 2) { + throw RuntimeException("Unable to parse the symbolic file entry $name") + } + name = split[0].trim() + link = split[1].trim() + } + + AndroidFile( + permissions = permissions, + owner = owner, + group = group, + size = size, + date = date, + time = time, + name = name, + directory = directory, + type = type, + link = link + ) + } + } +} + +enum class AndroidFileType { + REGULAR_FILE, + DIRECTORY, + BLOCK_SPECIAL_FILE, + CHARACTER_SPECIAL_FILE, + SYMBOLIC_LINK, + SOCKET_LINK, + FIFO, + OTHER, +} + +/** + * @property permissions full permissions string, e.g. -rw-rw---- + * @property owner file owner, e.g. root + * @property group file group, e.g. sdcard_rw + * @property date e.g. 2020-12-01 + * @property time e.g. 22:22 + * @property name the file name without path, e.g. testfile.txt + * @property directionality file's directory, e.g. /sdcard/ + * @property size file's size, e.g. 1024 + * @property type file's type + * @property link if the file is a symbolic link, this field is what the link points to + */ +data class AndroidFile( + val permissions: String, + val owner: String, + val group: String, + val date: String, + val time: String, + val name: String, + val directory: String, + val size: Long, + val type: AndroidFileType, + val link: String? = null +) diff --git a/src/main/kotlin/com/malinskiy/adam/request/testrunner/TestRunnerRequest.kt b/src/main/kotlin/com/malinskiy/adam/request/testrunner/TestRunnerRequest.kt index 340182818..36c5503c8 100644 --- a/src/main/kotlin/com/malinskiy/adam/request/testrunner/TestRunnerRequest.kt +++ b/src/main/kotlin/com/malinskiy/adam/request/testrunner/TestRunnerRequest.kt @@ -38,7 +38,7 @@ class TestRunnerRequest( private val buffer = ByteArray(Const.MAX_PACKET_LENGTH) override suspend fun readElement(readChannel: AndroidReadChannel, writeChannel: AndroidWriteChannel): String { - val available = readChannel.readAvailable(buffer, 0, Const.MAX_FILE_PACKET_LENGTH) + val available = readChannel.readAvailable(buffer, 0, Const.MAX_PACKET_LENGTH) return when { available > 0 -> { diff --git a/src/test/kotlin/com/malinskiy/adam/request/sync/ListFilesRequestTest.kt b/src/test/kotlin/com/malinskiy/adam/request/sync/ListFilesRequestTest.kt new file mode 100644 index 000000000..26f40d7be --- /dev/null +++ b/src/test/kotlin/com/malinskiy/adam/request/sync/ListFilesRequestTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2020 Anton Malinskiy + * + * 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.malinskiy.adam.request.sync + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEqualTo +import com.malinskiy.adam.Const +import com.malinskiy.adam.server.AndroidDebugBridgeServer +import io.ktor.utils.io.* +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class ListFilesRequestTest { + @Test + fun testReturnsProperContent() { + runBlocking { + val server = AndroidDebugBridgeServer() + + val client = server.startAndListen { input, output -> + val transportCmd = input.receiveCommand() + assertThat(transportCmd).isEqualTo("host:transport:serial") + output.respond(Const.Message.OKAY) + + val shellCmd = input.receiveCommand() + assertThat(shellCmd).isEqualTo("shell:ls -l /sdcard/") + output.respond(Const.Message.OKAY) + + val response = """ + total 88 + -rwxrwx--x 2 root sdcard_rw 4096 2020-10-24 16:29 Alarms + brwxrwx--x 4 root sdcard_rw 4096 2020-10-24 16:29 Android + lrwxrwx--x 2 root sdcard_rw 4096 2020-10-24 16:29 DCIM -> ../Camera + crwxrwx--x 2 root sdcard_rw 4096 2020-12-01 19:11 Download + srwxrwx--x 2 root sdcard_rw 4096 2020-10-24 16:29 Movies + prwxrwx--x 2 root sdcard_rw 4096 2020-10-24 16:29 Music + drwxrwx--x 2 root sdcard_rw 4096 2020-10-24 16:29 Ringtones + Orwxrwx--x 2 root sdcard_rw 4096 2020-10-24 16:29 XXX + """.trimIndent().toByteArray(Const.DEFAULT_TRANSPORT_ENCODING) + output.writeFully(response, 0, response.size) + output.close() + } + + val files = client.execute(ListFilesRequest("/sdcard/"), serial = "serial") + assertThat(files).containsExactly( + AndroidFile( + permissions = "-rwxrwx--x", + directory = "/sdcard/", + date = "2020-10-24", + group = "sdcard_rw", + link = null, + name = "Alarms", + owner = "root", + size = 4096, + time = "16:29", + type = AndroidFileType.REGULAR_FILE + ), + AndroidFile( + permissions = "brwxrwx--x", + directory = "/sdcard/", + date = "2020-10-24", + group = "sdcard_rw", + link = null, + name = "Android", + owner = "root", + size = 4096, + time = "16:29", + type = AndroidFileType.BLOCK_SPECIAL_FILE + ), + AndroidFile( + permissions = "lrwxrwx--x", + directory = "/sdcard/", + date = "2020-10-24", + group = "sdcard_rw", + link = "../Camera", + name = "DCIM", + owner = "root", + size = 4096, + time = "16:29", + type = AndroidFileType.SYMBOLIC_LINK + ), + AndroidFile( + permissions = "crwxrwx--x", + directory = "/sdcard/", + date = "2020-12-01", + group = "sdcard_rw", + link = null, + name = "Download", + owner = "root", + size = 4096, + time = "19:11", + type = AndroidFileType.CHARACTER_SPECIAL_FILE + ), + AndroidFile( + permissions = "srwxrwx--x", + directory = "/sdcard/", + date = "2020-10-24", + group = "sdcard_rw", + link = null, + name = "Movies", + owner = "root", + size = 4096, + time = "16:29", + type = AndroidFileType.SOCKET_LINK + ), + AndroidFile( + permissions = "prwxrwx--x", + directory = "/sdcard/", + date = "2020-10-24", + group = "sdcard_rw", + link = null, + name = "Music", + owner = "root", + size = 4096, + time = "16:29", + type = AndroidFileType.FIFO + ), + AndroidFile( + permissions = "drwxrwx--x", + directory = "/sdcard/", + date = "2020-10-24", + group = "sdcard_rw", + link = null, + name = "Ringtones", + owner = "root", + size = 4096, + time = "16:29", + type = AndroidFileType.DIRECTORY + ) + ) + + server.dispose() + } + } +} \ No newline at end of file