Android Debug Bridge client/analysis tool
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

276 lines
7.3 KiB

import net, strutils, parseutils, strformat, osproc, sequtils
import system, times, math, sugar, os, streams
import posix except Time
import options except map
type FileStat = ref object of RootObj
androidFileMode : BiggestInt
androidFileSize : uint32
androidFileModified : Time
androidFileName : string
type AndroidFile = ref object of RootObj
androidFileName : string
androidFileStat : FileStat
androidFileContents : string
proc chunkString(buf : string) : Option[seq[string]] =
if buf.len == 0:
return none(seq[string])
let chunkNum = (buf.len / (2^16-1)).ceil.BiggestInt
some(buf.toSeq.distribute(chunkNum, false).map(chunk => chunk.map(c => $char(c)).join))
proc recvExactly(socket : Socket, length : int) : string =
var buf = ""
while (buf.len != length):
buf &= socket.recv(length - buf.len)
buf
proc parseAdb(resp : string) : Option[string] =
if resp.len == 0:
return none(string)
var msg_len : int
var offset : int
let status = resp[0..3]
let loc = parseHex(resp, msg_len, 4, 4)
if msg_len > 0:
offset = loc + 4 + msg_len - 1
else:
offset = resp.len - 1
var msg = resp[loc+4..offset]
if status == "FAIL":
stderr.writeLine(msg)
return none(string)
some(msg)
proc makeMsg(msg : string) : string =
fmt"{msg.len:04x}{msg}"
proc adbConnect() : Socket =
let socket = newSocket(buffered=false)
socket.connect("127.0.0.1", Port(5037))
socket
proc unrollBytes(n : uint32) : string =
let shifts : seq[uint32] = @[0'u32, 8'u32, 16'u32, 24'u32]
# shift each byte right by a certain amount and mask off the least-significant byte
map(shifts, shift => $char((n shr shift) and 0x000000ff)).join
proc rollBytes(bs : string) : uint32 =
let shifts : seq[uint32] = @[0'u32, 8'u32, 16'u32, 24'u32]
var n : uint32
let pairs : seq[tuple[a: uint32, b: char]] = zip(shifts, bs)
for pair in pairs:
n = n or pair.b.uint32 shl pair.a
n
proc syncMode(): Socket =
let socket = adbConnect()
socket.send("host:transport-usb".makeMsg)
discard socket.recvExactly(4)
socket.send("sync:".makeMsg)
discard socket.recvExactly(4).parseAdb.get
socket
proc listDir(filename : string) : seq[FileStat] =
let socket : Socket = syncMode()
let filenameLen : string = filename.len.uint32.unrollBytes
var dirents : seq[FileStat] = @[]
var dirent : string
var status : string
socket.send("LIST" & filenameLen & filename)
while(status = socket.recvExactly(4); status != "DONE"):
dirent = socket.recvExactly(16)
let fileMode = dirent[0..3].rollBytes.BiggestInt
let fileSize = dirent[4..7].rollBytes
let fileCreated = dirent[8..11].rollBytes.int64.fromUnix
let fileNameLen = dirent[12..15].rollBytes.int
let direntFileName = socket.recvExactly(filenameLen)
dirents &= FileStat(androidFileName: direntFileName,
androidFileMode: fileMode,
androidFileSize: fileSize,
androidFileModified: fileCreated)
dirents
proc recvFile(filename : string) : Option[string] =
# Enter sync mode
let socket : Socket = syncMode()
let filenameLen : string = filename.len.uint32.unrollBytes
socket.send("RECV" & filenameLen & filename)
var recvResult : string
var status : string = ""
var fileBody : string
var recvBody : string
var fileLen : int
var buf = ""
while (status != "DONE"):
recvResult = socket.recvExactly(8)
status = recvResult[0..3]
fileLen = recvResult[4..7].rollBytes.int
if (fileLen == 0 or status == "DONE"):
break
if status == "FAIL":
# Return early if we failed
socket.close()
return none(string)
recvBody = ""
assert(status == "DATA")
assert(fileLen <= 0xffff and fileLen > 0, "File Length Should be <=65535 and > 0")
recvBody = socket.recvExactly(fileLen)
assert(recvBody.len == fileLen)
fileBody = recvBody[0..fileLen - 1]
buf &= fileBody
assert(status == "DONE")
assert(fileLen == 0)
socket.close()
some(buf)
proc statFile(filename : string) : Option[FileStat] =
# Enter sync mode
let socket : Socket = syncMode()
let filenameLen : string = filename.len.uint32.unrollBytes
socket.send("STAT" & filenameLen & filename)
let statResult : string = socket.recvExactly(16)
let command = map(statResult[0..3], c => $char(c)).join
let fileMode = statResult[4..7].rollBytes.BiggestInt
let fileSize = statResult[8..11].rollBytes
let fileCreated = statResult[12..15].rollBytes.int64.fromUnix
socket.close()
if (fileMode != 0 and fileSize != 0):
some(FileStat(androidFileName: filename,
androidFileMode: fileMode,
androidFileSize: fileSize,
androidFileModified: fileCreated))
else:
none(FileStat)
proc adbSend(buf : string,
destination : string,
permissions : string,
overwrite = false) : bool =
let stat = destination.statFile
if stat.isSome and (not overwrite):
# never overwrite files unless asked to
return false
# Enter sync mode
let socket : Socket = syncMode()
let fileMode = fromOct[int](fmt"0{permissions}")
let lmtime = getTime().toUnix.uint32.unrollBytes
let remoteFileName = fmt"{destination},{fileMode:04o}"
let chunks = buf.chunkString
if chunks.isNone:
return false
socket.send("SEND" & remoteFileName.len.uint32.unrollBytes & remoteFileName)
for chunk in chunks.get:
socket.send("DATA" & chunk.len.uint32.unrollBytes & chunk)
socket.send("DONE" & lmtime)
let serverResp = socket.recvExactly(4)
if serverResp == "FAIL":
let errorMsgLen = socket.recvExactly(4).rollBytes
let errorMsg = socket.recvExactly(errorMsgLen.int)
stderr.writeLine errorMsg
socket.close()
return false
assert(serverResp == "OKAY")
socket.close()
true
proc androidCopyFile*(filename : string, dest : string) =
var statResult : Stat
# See https://stackoverflow.com/a/14325854/903589 for an explanation of the bitmasks
let perms = statResult.st_mode.int and (S_IRWXU or S_IRWXG or S_IRWXO)
discard filename.readFile.adbSend(dest, fmt"0{perms}", overwrite = true)
proc adbPull(filename : string) : Option[AndroidFile] =
let stat = filename.statFile
if stat.isNone:
return none(AndroidFile)
let fileBlob = filename.recvFile.get("")
some(AndroidFile(androidFileName: filename,
androidFileStat: stat.get,
androidFileContents: fileBlob))
proc runCommand(payload : string) : string =
let socket = adbConnect()
socket.send("host:transport-usb".makeMsg)
discard socket.recvExactly(4)
socket.send(payload)
var response = ""
while (var chunk = socket.recv(1024); chunk != ""):
# receive chunks until it returns nothing
response &= chunk
socket.close()
response
proc rebootPhone() : Option[string] =
makeMsg("reboot:").runCommand.parseAdb
proc listCerts*() : string =
makeMsg("shell:ls -1a /etc/security/cacerts/*").runCommand.parseAdb.get
proc devices() : Option[string] =
makeMsg("host:version").runCommand.parseAdb
proc parseCerts() =
for cacert in listCerts().split("\n"):
let certfile = adbPull(cacert)
if certfile.isSome:
let filename = cacert.extractFilename
echo "Downloading " & filename
var fileContents = certfile.get.androidFileContents
var certFileStream = fileContents.newStringStream
proc startServer*() = discard execCmd("adb start-server")