Skip to content

Commit c02053f

Browse files
Merkostclaude
andcommitted
fix(adb): correct STA2 parser — fixed-size struct, not length envelope
Root cause of all the framing violations seen against real devices: the prior parser treated STA2 response as 'id + uint32 length + body'. The AOSP protocol is a packed sync_v2_stat struct sent verbatim — id is first, error is at +4 (not a length), and the body extends to byte 72. Reading error-as-length gave us 0 on success, then trying to read 0 bytes failed the count >= 24 guard. Introduces SyncV2Stat / SyncV1Stat / SyncV2Dent in a new ADBSyncStructs.swift with precise byte-offset decoders and a unit-test suite hand-crafting buffers that mirror adbd's output. statV2 now reads the full 72-byte struct atomically and decodes it via SyncV2Stat.decode. FAIL detection still works because FAIL DOES use the 'id + length + body' envelope, so we read length only after seeing 'FAIL'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9d63bd9 commit c02053f

4 files changed

Lines changed: 219 additions & 43 deletions

File tree

Sources/FreeDroidADB/Wire/ADBProtocolPackets.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,15 @@ extension Data {
3131
$0.loadUnaligned(as: UInt32.self).littleEndian
3232
}
3333
}
34+
35+
func readU64LE(at offset: Int) -> UInt64 {
36+
guard count >= offset + 8 else { return 0 }
37+
return subdata(in: offset..<offset + 8).withUnsafeBytes {
38+
$0.loadUnaligned(as: UInt64.self).littleEndian
39+
}
40+
}
41+
42+
func readI64LE(at offset: Int) -> Int64 {
43+
Int64(bitPattern: readU64LE(at: offset))
44+
}
3445
}

Sources/FreeDroidADB/Wire/ADBSyncClient.swift

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,25 @@ public actor ADBSyncClient {
135135

136136
let id = try await connection.readBytes(4)
137137
let idStr = String(decoding: id, as: UTF8.self)
138-
let length = try await connection.readU32LE()
139138
switch idStr {
140139
case "STA2":
141-
let entry = try await readSta2Entry(path: remotePath, length: Int(length))
142-
return entry
140+
let body = try await connection.readBytes(SyncV2Stat.wireSize - 4)
141+
var combined = Data()
142+
combined.append(id)
143+
combined.append(body)
144+
let stat = try SyncV2Stat.decode(idAndBody: combined)
145+
return SyncEntry(
146+
name: (remotePath as NSString).lastPathComponent,
147+
mode: stat.mode,
148+
size: stat.size,
149+
uid: stat.uid,
150+
gid: stat.gid,
151+
atime: UInt64(bitPattern: stat.atime),
152+
mtime: UInt64(bitPattern: stat.mtime),
153+
ctime: UInt64(bitPattern: stat.ctime)
154+
)
143155
case "FAIL":
156+
let length = try await connection.readU32LE()
144157
let msg = try await connection.readString(Int(length))
145158
throw ADBWireError.syncFailed(msg)
146159
default:
@@ -151,46 +164,6 @@ public actor ADBSyncClient {
151164
}
152165
}
153166

154-
private func readSta2Entry(path: String, length: Int) async throws -> SyncEntry {
155-
let payload = try await connection.readBytes(length)
156-
guard payload.count >= 24 else {
157-
throw ADBWireError.framingViolation(
158-
context: "STA2 payload < 24 bytes (got \(payload.count))",
159-
firstBytes: Array(payload.prefix(16))
160-
)
161-
}
162-
let mode = payload.readU32LE(at: 0)
163-
let size64 = payload.withUnsafeBytes { ptr -> UInt64 in
164-
guard ptr.count >= 16 else { return 0 }
165-
return ptr.loadUnaligned(fromByteOffset: 8, as: UInt64.self).littleEndian
166-
}
167-
let uid = payload.readU32LE(at: 16)
168-
let gid = payload.readU32LE(at: 20)
169-
let atime = payload.withUnsafeBytes { ptr -> UInt64 in
170-
guard ptr.count >= 32 else { return 0 }
171-
return ptr.loadUnaligned(fromByteOffset: 24, as: UInt64.self).littleEndian
172-
}
173-
let mtime = payload.withUnsafeBytes { ptr -> UInt64 in
174-
guard ptr.count >= 40 else { return 0 }
175-
return ptr.loadUnaligned(fromByteOffset: 32, as: UInt64.self).littleEndian
176-
}
177-
let ctime = payload.withUnsafeBytes { ptr -> UInt64 in
178-
guard ptr.count >= 48 else { return 0 }
179-
return ptr.loadUnaligned(fromByteOffset: 40, as: UInt64.self).littleEndian
180-
}
181-
let name = (path as NSString).lastPathComponent
182-
return SyncEntry(
183-
name: name,
184-
mode: mode,
185-
size: size64,
186-
uid: uid,
187-
gid: gid,
188-
atime: atime,
189-
mtime: mtime,
190-
ctime: ctime
191-
)
192-
}
193-
194167
public func recv(remotePath: String, to localURL: URL, progress: TransferProgressSink?) async throws -> Int64 {
195168
let pathData = Data(remotePath.utf8)
196169
var req = Data()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import Foundation
2+
3+
struct SyncV2Stat: Sendable {
4+
let mode: UInt32
5+
let nlink: UInt32
6+
let uid: UInt32
7+
let gid: UInt32
8+
let size: UInt64
9+
let atime: Int64
10+
let mtime: Int64
11+
let ctime: Int64
12+
13+
var isDirectory: Bool { (mode & 0o170000) == 0o040000 }
14+
var isSymlink: Bool { (mode & 0o170000) == 0o120000 }
15+
16+
static let wireSize = 72
17+
18+
static func decode(idAndBody data: Data) throws -> SyncV2Stat {
19+
guard data.count >= wireSize else {
20+
throw ADBWireError.framingViolation(
21+
context: "SyncV2Stat needs \(wireSize) bytes, got \(data.count)",
22+
firstBytes: Array(data.prefix(16))
23+
)
24+
}
25+
let id = data.subdata(in: 0..<4)
26+
guard id == Data("STA2".utf8) else {
27+
throw ADBWireError.framingViolation(
28+
context: "SyncV2Stat id != STA2",
29+
firstBytes: Array(data.prefix(16))
30+
)
31+
}
32+
let error = data.readU32LE(at: 4)
33+
if error != 0 {
34+
throw ADBWireError.syncFailed("STA2 errno \(error)")
35+
}
36+
return SyncV2Stat(
37+
mode: data.readU32LE(at: 24),
38+
nlink: data.readU32LE(at: 28),
39+
uid: data.readU32LE(at: 32),
40+
gid: data.readU32LE(at: 36),
41+
size: data.readU64LE(at: 40),
42+
atime: data.readI64LE(at: 48),
43+
mtime: data.readI64LE(at: 56),
44+
ctime: data.readI64LE(at: 64)
45+
)
46+
}
47+
}
48+
49+
struct SyncV2Dent: Sendable {
50+
let name: String
51+
let mode: UInt32
52+
let uid: UInt32
53+
let gid: UInt32
54+
let size: UInt64
55+
let atime: Int64
56+
let mtime: Int64
57+
let ctime: Int64
58+
59+
var isDirectory: Bool { (mode & 0o170000) == 0o040000 }
60+
var isSymlink: Bool { (mode & 0o170000) == 0o120000 }
61+
62+
static let fixedHeaderSize = 76
63+
static let bodyAfterId = 72
64+
}
65+
66+
struct SyncV1Stat: Sendable {
67+
let mode: UInt32
68+
let size: UInt64
69+
let mtime: Int64
70+
71+
var isDirectory: Bool { (mode & 0o170000) == 0o040000 }
72+
var isSymlink: Bool { (mode & 0o170000) == 0o120000 }
73+
74+
static let wireSize = 16
75+
76+
static func decode(idAndBody data: Data) throws -> SyncV1Stat {
77+
guard data.count >= wireSize else {
78+
throw ADBWireError.framingViolation(
79+
context: "SyncV1Stat needs \(wireSize) bytes, got \(data.count)",
80+
firstBytes: Array(data.prefix(16))
81+
)
82+
}
83+
let id = data.subdata(in: 0..<4)
84+
guard id == Data("STAT".utf8) else {
85+
throw ADBWireError.framingViolation(
86+
context: "SyncV1Stat id != STAT",
87+
firstBytes: Array(data.prefix(16))
88+
)
89+
}
90+
return SyncV1Stat(
91+
mode: data.readU32LE(at: 4),
92+
size: UInt64(data.readU32LE(at: 8)),
93+
mtime: Int64(data.readU32LE(at: 12))
94+
)
95+
}
96+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import Foundation
2+
import Testing
3+
@testable import FreeDroidADB
4+
5+
@Suite("SyncProtocol decode")
6+
struct SyncProtocolDecodeTests {
7+
@Test func decodesRealSta2Frame() throws {
8+
var buf = Data()
9+
buf.append(contentsOf: "STA2".utf8)
10+
buf.appendU32LE(0)
11+
buf.append(contentsOf: [UInt8](repeating: 0, count: 8))
12+
buf.append(contentsOf: [UInt8](repeating: 0, count: 8))
13+
buf.appendU32LE(0o100644)
14+
buf.appendU32LE(1)
15+
buf.appendU32LE(1000)
16+
buf.appendU32LE(1015)
17+
appendU64LE(into: &buf, 5347282)
18+
appendI64LE(into: &buf, 1721880000)
19+
appendI64LE(into: &buf, 1721880120)
20+
appendI64LE(into: &buf, 1721880000)
21+
22+
let stat = try SyncV2Stat.decode(idAndBody: buf)
23+
#expect(stat.mode == 0o100644)
24+
#expect(stat.size == 5347282)
25+
#expect(stat.mtime == 1721880120)
26+
#expect(stat.isDirectory == false)
27+
}
28+
29+
@Test func decodesDirectorySta2Frame() throws {
30+
var buf = Data()
31+
buf.append(contentsOf: "STA2".utf8)
32+
buf.appendU32LE(0)
33+
buf.append(contentsOf: [UInt8](repeating: 0, count: 16))
34+
buf.appendU32LE(0o040755)
35+
buf.appendU32LE(2)
36+
buf.appendU32LE(0)
37+
buf.appendU32LE(0)
38+
appendU64LE(into: &buf, 4096)
39+
appendI64LE(into: &buf, 0)
40+
appendI64LE(into: &buf, 1700000000)
41+
appendI64LE(into: &buf, 0)
42+
43+
let stat = try SyncV2Stat.decode(idAndBody: buf)
44+
#expect(stat.isDirectory)
45+
#expect(stat.mtime == 1700000000)
46+
}
47+
48+
@Test func throwsOnSta2WithErrno() throws {
49+
var buf = Data()
50+
buf.append(contentsOf: "STA2".utf8)
51+
buf.appendU32LE(2)
52+
buf.append(contentsOf: [UInt8](repeating: 0, count: 64))
53+
54+
#expect(throws: ADBWireError.self) {
55+
_ = try SyncV2Stat.decode(idAndBody: buf)
56+
}
57+
}
58+
59+
@Test func throwsOnSta2TooShort() {
60+
let buf = Data("STA2".utf8)
61+
#expect(throws: ADBWireError.self) {
62+
_ = try SyncV2Stat.decode(idAndBody: buf)
63+
}
64+
}
65+
66+
@Test func throwsOnSta2WrongId() {
67+
var buf = Data()
68+
buf.append(contentsOf: "XXXX".utf8)
69+
buf.append(contentsOf: [UInt8](repeating: 0, count: 68))
70+
#expect(throws: ADBWireError.self) {
71+
_ = try SyncV2Stat.decode(idAndBody: buf)
72+
}
73+
}
74+
75+
@Test func decodesSta1Frame() throws {
76+
var buf = Data()
77+
buf.append(contentsOf: "STAT".utf8)
78+
buf.appendU32LE(0o100755)
79+
buf.appendU32LE(12345)
80+
buf.appendU32LE(1700000000)
81+
let stat = try SyncV1Stat.decode(idAndBody: buf)
82+
#expect(stat.mode == 0o100755)
83+
#expect(stat.size == 12345)
84+
#expect(stat.mtime == 1700000000)
85+
}
86+
87+
private func appendU64LE(into buf: inout Data, _ value: UInt64) {
88+
var v = value.littleEndian
89+
Swift.withUnsafeBytes(of: &v) { buf.append(contentsOf: $0) }
90+
}
91+
92+
private func appendI64LE(into buf: inout Data, _ value: Int64) {
93+
var v = value.littleEndian
94+
Swift.withUnsafeBytes(of: &v) { buf.append(contentsOf: $0) }
95+
}
96+
}

0 commit comments

Comments
 (0)