feat(av-live-body): usbmux device client

Add USBClient for usbmux device discovery and connect-to-port,
with an injectable MuxTransport so tests need no real device.

Harden USBMuxProtocol.readLE32 to return an optional with a
bounds check, avoiding an out-of-range crash on truncated data.
This commit is contained in:
L'électron rare
2026-05-18 16:03:30 +02:00
parent e70595d81a
commit ebed40c98a
4 changed files with 101 additions and 2 deletions
@@ -0,0 +1,49 @@
import Foundation
/// Transport abstraction over the usbmuxd Unix socket. The real
/// implementation wraps a `socket(AF_UNIX)`; tests inject a mock.
protocol MuxTransport {
func send(_ data: Data)
func receivePacket() -> Data?
func close()
}
/// usbmux client: device discovery + connect-to-port. After a
/// successful `connect`, the same transport carries the raw tunneled
/// byte stream from the device.
final class USBClient {
private let transport: MuxTransport
private var tag: UInt32 = 0
init(transport: MuxTransport) {
self.transport = transport
}
func listDevices() -> [Int] {
tag += 1
transport.send(USBMuxProtocol.encode(
plist: ["MessageType": "ListDevices"], tag: tag))
guard let reply = transport.receivePacket(),
let plist = USBMuxProtocol.decode(reply),
let list = plist["DeviceList"] as? [[String: Any]]
else { return [] }
return list.compactMap { $0["DeviceID"] as? Int }
}
/// Returns true once the transport is tunneled to `port` on the
/// device. usbmux wants the TCP port in big-endian order.
func connect(deviceID: Int, port: UInt16) -> Bool {
tag += 1
let swapped = Int((port << 8) | (port >> 8))
transport.send(USBMuxProtocol.encode(plist: [
"MessageType": "Connect",
"DeviceID": deviceID,
"PortNumber": swapped,
], tag: tag))
guard let reply = transport.receivePacket(),
let plist = USBMuxProtocol.decode(reply),
let number = plist["Number"] as? Int
else { return false }
return number == 0
}
}
@@ -28,7 +28,8 @@ enum USBMuxProtocol {
for i in 0..<4 { d.append(UInt8((v >> (8 * i)) & 0xFF)) }
}
static func readLE32(_ d: Data, _ offset: Int) -> UInt32 {
static func readLE32(_ d: Data, _ offset: Int) -> UInt32? {
guard offset >= 0, d.count >= offset + 4 else { return nil }
let b = [UInt8](d)
var v: UInt32 = 0
for i in 0..<4 { v |= UInt32(b[offset + i]) << (8 * i) }
@@ -0,0 +1,49 @@
import XCTest
@testable import AVLiveBody
/// In-memory stand-in for the usbmuxd Unix socket.
final class MockMuxTransport: MuxTransport {
var sent: [Data] = []
var canned: [Data] = []
func send(_ data: Data) { sent.append(data) }
func receivePacket() -> Data? {
canned.isEmpty ? nil : canned.removeFirst()
}
func close() {}
}
final class USBClientTests: XCTestCase {
func testListDevicesParsesDeviceIDs() {
let mock = MockMuxTransport()
mock.canned = [USBMuxProtocol.encode(plist: [
"DeviceList": [
["DeviceID": 42,
"Properties": ["ConnectionType": "USB"]],
]], tag: 0)]
let client = USBClient(transport: mock)
let devices = client.listDevices()
XCTAssertEqual(devices, [42])
}
func testConnectSendsConnectRequest() {
let mock = MockMuxTransport()
mock.canned = [USBMuxProtocol.encode(
plist: ["MessageType": "Result", "Number": 0], tag: 0)]
let client = USBClient(transport: mock)
let ok = client.connect(deviceID: 42, port: 7000)
XCTAssertTrue(ok)
let req = USBMuxProtocol.decode(mock.sent.last!)
XCTAssertEqual(req?["MessageType"] as? String, "Connect")
XCTAssertEqual(req?["DeviceID"] as? Int, 42)
XCTAssertEqual(req?["PortNumber"] as? Int,
Int((UInt16(7000) << 8) | (UInt16(7000) >> 8)))
}
func testConnectFailsOnNonZeroResult() {
let mock = MockMuxTransport()
mock.canned = [USBMuxProtocol.encode(
plist: ["MessageType": "Result", "Number": 3], tag: 0)]
let client = USBClient(transport: mock)
XCTAssertFalse(client.connect(deviceID: 1, port: 7000))
}
}
@@ -6,7 +6,7 @@ final class USBMuxProtocolTests: XCTestCase {
let body: [String: Any] = ["MessageType": "ListDevices"]
let packet = USBMuxProtocol.encode(plist: body, tag: 3)
XCTAssertGreaterThan(packet.count, 16)
XCTAssertEqual(Int(USBMuxProtocol.readLE32(packet, 0)),
XCTAssertEqual(USBMuxProtocol.readLE32(packet, 0).map(Int.init),
packet.count)
XCTAssertEqual(USBMuxProtocol.readLE32(packet, 4), 1)
XCTAssertEqual(USBMuxProtocol.readLE32(packet, 8), 8)