adbproxy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. adbproxy/__init__.py +2 -0
  2. adbproxy/__main__.py +5 -0
  3. adbproxy/cli/__init__.py +1 -0
  4. adbproxy/cli/device.py +13 -0
  5. adbproxy/cli/main.py +60 -0
  6. adbproxy/cli/server.py +13 -0
  7. adbproxy/config.py +39 -0
  8. adbproxy/device/__init__.py +1 -0
  9. adbproxy/device/app.py +532 -0
  10. adbproxy/device/backend.py +242 -0
  11. adbproxy/device/banner.py +33 -0
  12. adbproxy/device/connection.py +505 -0
  13. adbproxy/device/session.py +304 -0
  14. adbproxy/device/stream_table.py +125 -0
  15. adbproxy/observers/__init__.py +1 -0
  16. adbproxy/observers/events.py +19 -0
  17. adbproxy/observers/install.py +126 -0
  18. adbproxy/observers/shell.py +371 -0
  19. adbproxy/observers/stream.py +94 -0
  20. adbproxy/observers/sync.py +550 -0
  21. adbproxy/protocols/__init__.py +1 -0
  22. adbproxy/protocols/adb.py +66 -0
  23. adbproxy/protocols/payload/__init__.py +17 -0
  24. adbproxy/protocols/payload/install.py +44 -0
  25. adbproxy/protocols/payload/shell_v2.py +28 -0
  26. adbproxy/protocols/payload/sync.py +180 -0
  27. adbproxy/protocols/smart_socket/__init__.py +33 -0
  28. adbproxy/protocols/smart_socket/classifier.py +78 -0
  29. adbproxy/protocols/smart_socket/conversation.py +335 -0
  30. adbproxy/protocols/smart_socket/handshake.py +75 -0
  31. adbproxy/protocols/smart_socket/wire.py +82 -0
  32. adbproxy/protocols/transport/__init__.py +52 -0
  33. adbproxy/protocols/transport/codec.py +90 -0
  34. adbproxy/protocols/transport/constants.py +21 -0
  35. adbproxy/protocols/transport/errors.py +5 -0
  36. adbproxy/runtime/__init__.py +1 -0
  37. adbproxy/runtime/adb_process.py +195 -0
  38. adbproxy/runtime/capture.py +41 -0
  39. adbproxy/runtime/logging.py +113 -0
  40. adbproxy/runtime/sockets.py +14 -0
  41. adbproxy/server/__init__.py +1 -0
  42. adbproxy/server/app.py +221 -0
  43. adbproxy/server/connection.py +378 -0
  44. adbproxy-0.1.0.dist-info/METADATA +254 -0
  45. adbproxy-0.1.0.dist-info/RECORD +49 -0
  46. adbproxy-0.1.0.dist-info/WHEEL +5 -0
  47. adbproxy-0.1.0.dist-info/entry_points.txt +4 -0
  48. adbproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
  49. adbproxy-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,550 @@
1
+ import os
2
+ import re
3
+ import struct
4
+ import threading
5
+
6
+ from adbproxy.config import (
7
+ MAX_SYNC_FILE,
8
+ _DENT_V1_SIZE,
9
+ _DENT_V2_FMT,
10
+ _DENT_V2_SIZE,
11
+ _STAT_V1_SIZE,
12
+ _STAT_V2_FMT,
13
+ _STAT_V2_SIZE,
14
+ )
15
+ from adbproxy.protocols.payload.sync import SyncWireDecoder, plan_sync_frame
16
+ from adbproxy.runtime.capture import open_unique_capture, reserve_unique_capture_path
17
+
18
+
19
+ SYNC_DATA_MAX = 64 * 1024
20
+
21
+
22
+ class SyncSession:
23
+ """per-conn sync 二进制协议状态机 + 文件捕获落盘。
24
+
25
+ 分帧由 pure ``SyncWireDecoder`` / ``plan_sync_frame`` 完成;本类持有
26
+ 方向状态、捕获与压缩旁路。
27
+ """
28
+
29
+ def __init__(self, logger, files_dir, capture_enabled=True):
30
+ self.logger = logger
31
+ self.files_dir = files_dir
32
+ self.capture_enabled = capture_enabled
33
+ self.lock = threading.Lock()
34
+ self.op = None # "SEND"=push / "RECV"=pull / None
35
+ self.remote_path = None
36
+ self.basename = None
37
+ self.remote_size = (
38
+ None # pull 的 STAT/STA2 响应已解析 size(命名用,pull 不取 mtime)
39
+ )
40
+ self.accumulated = 0 # 已收文件字节数(P4:解压后长度,命名 size 段用)
41
+ self.file_fp = None # 落盘文件句柄("wb" 覆写,时间戳防重名)
42
+ self.capture_path = None
43
+ # sync 是双向协议:两个方向必须各自重组半帧,不能用同一缓冲拼接。
44
+ self.c_wire = SyncWireDecoder()
45
+ self.s_wire = SyncWireDecoder()
46
+ # 兼容测试/调用方:暴露 frame_buf 视图别名(指向 pure codec 缓冲)
47
+ self.c_frame_buf = self.c_wire.buf
48
+ self.s_frame_buf = self.s_wire.buf
49
+ self.c_bypass = False
50
+ self.s_bypass = False
51
+ self.bypass = False # 兼容汇总标志;解析时仅禁用发生错误的方向。
52
+ self.expecting = None # "req"/"data_c"/"data_s"/"idle"/None
53
+ self.awaiting_details = (
54
+ None # v2: None / "send" / "recv"(SND2/RCV2 双发第二帧判据)
55
+ )
56
+ # P4: sync v2 压缩旁路解压(仅作用于落盘 _write_capture,sendall 原样压缩字节透传)
57
+ self.compression = None # None/"zstd"/"lz4"/"brotli"
58
+ self.decoder = None # 流式解压器实例(zstandard.ZstdDecompressionObj / lz4.frame / brotli.Decompressor)
59
+ self.decoder_lib_missing = (
60
+ False # 解压库未装/异常降级标志(原样落盘压缩字节 + warning)
61
+ )
62
+ self.decoder_fallback = None # None/"unavailable"/"failed"
63
+ self.bypass = False # 解析异常降级标志
64
+ self.capture_overflow = False # 单文件超 MAX_SYNC_FILE 后停止写盘
65
+
66
+ @property
67
+ def active(self):
68
+ return self.expecting is not None
69
+
70
+ def activate(self):
71
+ """由 Connection 在 client 首帧 service 命中 sync: 时调用,显式置 expecting 打破循环依赖。"""
72
+ with self.lock:
73
+ self.expecting = "req"
74
+ self.op = None
75
+ self.awaiting_details = None
76
+ self.c_wire.reset()
77
+ self.s_wire.reset()
78
+ self.c_frame_buf = self.c_wire.buf
79
+ self.s_frame_buf = self.s_wire.buf
80
+ self.c_bypass = False
81
+ self.s_bypass = False
82
+ self.bypass = False
83
+
84
+ def on_client(self, data):
85
+ with self.lock:
86
+ self._feed(data, "c")
87
+
88
+ def on_server(self, data):
89
+ with self.lock:
90
+ self._feed(data, "s")
91
+
92
+ def _feed(self, data, side):
93
+ """按 (id,方向,状态) 分帧。pure codec 持缓冲;每帧后刷新 awaiting_details。"""
94
+ bypass_attr = "c_bypass" if side == "c" else "s_bypass"
95
+ wire = self.c_wire if side == "c" else self.s_wire
96
+ if getattr(self, bypass_attr) or wire.bypass:
97
+ return
98
+ wire.extend(data)
99
+ while True:
100
+ frame = wire.try_take_frame(
101
+ lambda id_, s=side: plan_sync_frame(id_, s, self.awaiting_details)
102
+ )
103
+ if frame is None:
104
+ if wire.bypass:
105
+ setattr(self, bypass_attr, True)
106
+ self.bypass = self.c_bypass or self.s_bypass
107
+ reason = wire.degrade_reason or "framing"
108
+ self.logger.debug(
109
+ f"[!] sync {reason} (side={side}, expecting={self.expecting}, "
110
+ f"awaiting={self.awaiting_details}), degrading"
111
+ )
112
+ return
113
+ self._on_frame(frame.id, frame.raw, side)
114
+ if getattr(self, bypass_attr):
115
+ return
116
+
117
+ def _on_frame(self, id_, frame, side):
118
+ if side == "c":
119
+ self._on_client_frame(id_, frame)
120
+ else:
121
+ self._on_server_frame(id_, frame)
122
+
123
+ def _on_client_frame(self, id_, frame):
124
+ if id_ in (b"STA2", b"LST2", b"STAT", b"LIST"):
125
+ # stat/list 查询请求:8+path,无文件落盘语义,保持 expecting 不变
126
+ path = frame[8:].decode("utf-8", errors="ignore")
127
+ self.logger.debug(f"[sync {id_.decode()} req]: path={path}")
128
+ return
129
+ if id_ == b"SEND": # v1 push 路径请求
130
+ path = frame[8:].decode("utf-8", errors="ignore")
131
+ if "," in path: # v1 SEND path 末尾带 ,mode
132
+ path = path.rsplit(",", 1)[0]
133
+ self._start_push(path, v2=False)
134
+ return
135
+ if id_ == b"RECV": # v1 pull 路径请求
136
+ path = frame[8:].decode("utf-8", errors="ignore")
137
+ self._start_pull(path, v2=False)
138
+ return
139
+ if id_ == b"SND2":
140
+ if self.awaiting_details == "send":
141
+ # 双发第二帧 = sync_send_v2(12B id+mode+flags)
142
+ mode = struct.unpack_from("<I", frame, 4)[0]
143
+ flags = struct.unpack_from("<I", frame, 8)[0]
144
+ self.logger.debug(f"[sync send_v2 details]: mode={mode} flags={flags}")
145
+ # P4: push DATA 是 client→server 压缩字节,proxy 旁路解压落盘(sendall 原样透传)
146
+ if not self._setup_decoder(flags):
147
+ self.c_bypass = True
148
+ self.bypass = self.c_bypass or self.s_bypass
149
+ return
150
+ self.awaiting_details = None
151
+ self.expecting = "data_c"
152
+ else:
153
+ path = frame[8:].decode("utf-8", errors="ignore")
154
+ self._start_push(path, v2=True)
155
+ return
156
+ if id_ == b"RCV2":
157
+ if self.awaiting_details == "recv":
158
+ flags = struct.unpack_from("<I", frame, 4)[0]
159
+ self.logger.debug(f"[sync recv_v2 details]: flags={flags}")
160
+ # P4: pull DATA 是 server→client 压缩字节,proxy 旁路解压落盘(sendall 原样透传)
161
+ if not self._setup_decoder(flags):
162
+ self.c_bypass = True
163
+ self.bypass = self.c_bypass or self.s_bypass
164
+ return
165
+ self.awaiting_details = None
166
+ self.expecting = "data_s"
167
+ else:
168
+ path = frame[8:].decode("utf-8", errors="ignore")
169
+ self._start_pull(path, v2=True)
170
+ return
171
+ if id_ == b"DATA":
172
+ if self.expecting == "data_c":
173
+ size = struct.unpack_from("<I", frame, 4)[0]
174
+ payload = frame[8 : 8 + size]
175
+ # P4: 旁路解压后写盘 + accumulated 累加解压后长度(sendall 原样压缩字节已更早透传)
176
+ self._feed_data(payload)
177
+ return
178
+ if id_ == b"DONE":
179
+ # push 文件结束(8B 无 payload,length 字段=mtime)
180
+ if self.expecting == "data_c":
181
+ mtime = struct.unpack_from("<I", frame, 4)[0]
182
+ # P4: DONE 时取解压器尾部字节一并写盘(sendall 原样透传不受影响)
183
+ self._flush_decoder()
184
+ self.logger.event(
185
+ f"[sync push done]: {self.basename} size={self.accumulated} mtime={mtime}"
186
+ )
187
+ # rename 在 _close_capture 内完成(临时名 -> {stem}-{accumulated}-{mtime}{ext}),
188
+ # 必须在 _reset_file_state 之前(此时 basename/accumulated 仍非 None)
189
+ self._close_capture(mtime=mtime)
190
+ self._reset_file_state()
191
+ return
192
+ if id_ == b"QUIT":
193
+ self._close_capture()
194
+ self.expecting = None
195
+ return
196
+ if id_ == b"FAIL":
197
+ msglen = struct.unpack_from("<I", frame, 4)[0]
198
+ msg = frame[8 : 8 + msglen].decode("utf-8", errors="ignore")
199
+ self.logger.event(f"[sync client FAIL]: {msg!r}")
200
+ return
201
+
202
+ def _on_server_frame(self, id_, frame):
203
+ if id_ in (b"STA2", b"LST2"):
204
+ # sync_stat_v2(72B):id,error,dev,ino,mode,nlink,uid,gid,size,atime,mtime,ctime
205
+ fields = struct.unpack(_STAT_V2_FMT, frame[:_STAT_V2_SIZE])
206
+ _id, error, _dev, _ino, _mode, _nlink, _uid, _gid, size, _at, _mt, _ct = (
207
+ fields
208
+ )
209
+ # pull 必先发 STAT_V2(AOSP file_sync_client.cpp:1008 sync_stat),proxy 在 RECV 前可见 size
210
+ # 命名用:pull 文件名 size 段取此字段;mtime(_mt) 不取(pull 用 python ts)
211
+ self.remote_size = size
212
+ self.logger.debug(f"[sync {id_.decode()} resp]: error={error} size={size}")
213
+ return
214
+ if id_ == b"STAT":
215
+ # sync_stat_v1(16B)
216
+ _id, mode, size, mtime = struct.unpack("<IIII", frame[:_STAT_V1_SIZE])
217
+ # pull 的 v1 stat 响应:同样存 size 供命名;mtime 不取
218
+ self.remote_size = size
219
+ self.logger.debug(
220
+ f"[sync lstat_v1 resp]: mode={mode} size={size} mtime={mtime}"
221
+ )
222
+ return
223
+ if id_ in (b"DNT2", b"DENT"):
224
+ # dent 头 + namelen 字节 name,目录项查询无文件落盘语义
225
+ if id_ == b"DNT2":
226
+ fields = struct.unpack(_DENT_V2_FMT, frame[:_DENT_V2_SIZE])
227
+ namelen = fields[-1]
228
+ name = frame[_DENT_V2_SIZE : _DENT_V2_SIZE + namelen].decode(
229
+ "utf-8", errors="ignore"
230
+ )
231
+ else:
232
+ _id, _mode, _size, _mt, namelen = struct.unpack(
233
+ "<IIIII", frame[:_DENT_V1_SIZE]
234
+ )
235
+ name = frame[_DENT_V1_SIZE : _DENT_V1_SIZE + namelen].decode(
236
+ "utf-8", errors="ignore"
237
+ )
238
+ self.logger.debug(f"[sync {id_.decode()}]: name={name}")
239
+ return
240
+ if id_ == b"DATA":
241
+ if self.expecting == "data_s":
242
+ size = struct.unpack_from("<I", frame, 4)[0]
243
+ payload = frame[8 : 8 + size]
244
+ # P4: 旁路解压后写盘 + accumulated 累加解压后长度(sendall 原样压缩字节已更早透传)
245
+ self._feed_data(payload)
246
+ return
247
+ if id_ == b"DONE":
248
+ # pull 文件结束(8B 无 payload,length=0);list 流也以 DONE 收尾
249
+ if self.expecting == "data_s":
250
+ # P4: DONE 时取解压器尾部字节一并写盘(sendall 原样透传不受影响)
251
+ self._flush_decoder()
252
+ self.logger.event(
253
+ f"[sync pull done]: {self.basename} size={self.accumulated}"
254
+ )
255
+ self._close_capture()
256
+ self._reset_file_state()
257
+ else:
258
+ self.logger.debug("[sync DONE (list/stat end)]")
259
+ return
260
+ if id_ == b"OKAY":
261
+ # push 成功确认(client 已发 DONE 后 server 回 OKAY,8B 无 payload)
262
+ if self.file_fp:
263
+ self._close_capture()
264
+ self.expecting = "req"
265
+ self.awaiting_details = None
266
+ return
267
+ if id_ == b"FAIL":
268
+ msglen = struct.unpack_from("<I", frame, 4)[0]
269
+ msg = frame[8 : 8 + msglen].decode("utf-8", errors="ignore")
270
+ self.logger.event(f"[sync FAIL]: {msg!r}")
271
+ self._close_capture()
272
+ self._reset_file_state()
273
+ return
274
+
275
+ def _start_push(self, remote, v2):
276
+ self.op = "SEND"
277
+ self.remote_path = remote
278
+ self.basename = os.path.basename(remote) or "sync_file"
279
+ self.accumulated = 0
280
+ self.capture_overflow = False
281
+ # P4 Major 3: 深度防御——每文件开头重置解压器,防 v2-zstd push 中途 QUIT 后同连接
282
+ # v1 SEND 喂过时 decompressobj(_setup_decoder 仅在 v2 details 帧时调用,v1 不调)
283
+ self.compression = None
284
+ self.decoder = None
285
+ self.decoder_lib_missing = False
286
+ self.decoder_fallback = None
287
+ self._open_capture()
288
+ cap = self.capture_path if self.capture_enabled else "<disabled>"
289
+ self.logger.event(f"📤 [sync push]: remote={remote} local_capture={cap}")
290
+ if v2:
291
+ # 双发:path 帧已收,下一帧是 sync_send_v2 details struct
292
+ self.awaiting_details = "send"
293
+ # expecting 保持(path 请求可能在 "req"/"idle",details 帧后置 data_c)
294
+ else:
295
+ self.awaiting_details = None
296
+ self.expecting = "data_c"
297
+
298
+ def _start_pull(self, remote, v2):
299
+ self.op = "RECV"
300
+ self.remote_path = remote
301
+ self.basename = os.path.basename(remote) or "sync_file"
302
+ self.accumulated = 0
303
+ self.capture_overflow = False
304
+ # P4 Major 3: 深度防御——每文件开头重置解压器,防过时 decompressobj 串扰
305
+ self.compression = None
306
+ self.decoder = None
307
+ self.decoder_lib_missing = False
308
+ self.decoder_fallback = None
309
+ self._open_capture()
310
+ cap = self.capture_path if self.capture_enabled else "<disabled>"
311
+ self.logger.event(f"📥 [sync pull]: remote={remote} local_capture={cap}")
312
+ if v2:
313
+ self.awaiting_details = "recv"
314
+ else:
315
+ self.awaiting_details = None
316
+ self.expecting = "data_s"
317
+
318
+ # ----------------------------------------------------------------
319
+ # P4: sync v2 压缩旁路解压(仅作用于落盘 _write_capture,sendall 原样压缩字节透传)
320
+ # ----------------------------------------------------------------
321
+ def _setup_decoder(self, flags):
322
+ """按 sync v2 details flags 建流式解压器(旁路落盘用,sendall 原样压缩字节透传不变)。
323
+ flags & 4(kSyncFlagZstd) → zstandard;flags & 2 → lz4;flags & 1 → brotli。
324
+ import 失败/建库异常 → decoder_lib_missing 降级(原样落盘压缩字节 + warning)。"""
325
+ self.compression = None
326
+ self.decoder = None
327
+ self.decoder_lib_missing = False
328
+ self.decoder_fallback = None
329
+ compression_flags = flags & 0x7
330
+ if compression_flags & (compression_flags - 1):
331
+ self.logger.event(
332
+ f"[!] sync multiple compression flags: flags={flags}, degrading observer"
333
+ )
334
+ return False
335
+ if flags & 4:
336
+ self.compression = "zstd"
337
+ try:
338
+ import zstandard # 环境硬依赖(.venv 已装),失败降级原样落盘 + warning
339
+
340
+ self.decoder = zstandard.ZstdDecompressor().decompressobj()
341
+ except Exception as e:
342
+ self.decoder_lib_missing = True
343
+ self.decoder_fallback = "unavailable"
344
+ self.logger.event(
345
+ f"[!] sync 压缩库未装,落盘压缩字节(命名 size=压缩长度): zstandard: {e}"
346
+ )
347
+ elif flags & 2:
348
+ self.compression = "lz4"
349
+ try:
350
+ import lz4.frame
351
+
352
+ self.decoder = lz4.frame.LZ4FrameDecompressor()
353
+ except Exception as e:
354
+ self.decoder_lib_missing = True
355
+ self.decoder_fallback = "unavailable"
356
+ self.logger.event(
357
+ f"[!] sync 压缩库未装,落盘压缩字节(命名 size=压缩长度): lz4: {e}"
358
+ )
359
+ elif flags & 1:
360
+ self.compression = "brotli"
361
+ try:
362
+ import brotli
363
+
364
+ self.decoder = brotli.Decompressor()
365
+ except Exception as e:
366
+ self.decoder_lib_missing = True
367
+ self.decoder_fallback = "unavailable"
368
+ self.logger.event(
369
+ f"[!] sync 压缩库未装,落盘压缩字节(命名 size=压缩长度): brotli: {e}"
370
+ )
371
+ # flags 无压缩位 → compression=None, decoder=None(DATA 原样落盘)
372
+ return True
373
+
374
+ def _feed_data(self, payload):
375
+ """DATA 帧旁路落盘:若已建流式解压器则先解压再写盘 + accumulated 累加解压后长度;
376
+ 库未装/已异常降级 → 原样落盘压缩字节(size=压缩长度);无压缩位 → 原样。
377
+ sendall 原样压缩字节透传(在更早已执行),此处仅旁路落盘。解压异常降级不阻断转发。
378
+ 解压方法按库统一取:zstd/lz4 用 .decompress,brotli 用 .process(brotli.Decompressor 无 decompress)。"""
379
+ if self.decoder is not None:
380
+ try:
381
+ decompress = getattr(self.decoder, "decompress", None) or getattr(
382
+ self.decoder, "process", None
383
+ )
384
+ payload = decompress(payload)
385
+ except Exception as e:
386
+ self.logger.event(f"[!] sync 解压异常,降级原样落盘: {e}")
387
+ self.decoder = None
388
+ self.decoder_lib_missing = True
389
+ self.decoder_fallback = "failed"
390
+ # decoder None(无压缩/库未装/异常降级)→ payload 原样;decoder 非空 → payload 已是解压后字节
391
+ self._write_capture(payload)
392
+ self.accumulated += len(payload)
393
+
394
+ def _flush_decoder(self):
395
+ """DONE 时取解压器尾部字节一并写盘(zstandard flush() 返回尾部,.eof 校验完成)。
396
+ lz4/brotli 无 flush 接口(decompress() 已返回全部),getattr 守卫跳过。
397
+ 异常降级不阻断(sendall 原样透传已在更早执行)。"""
398
+ if self.decoder is None:
399
+ return
400
+ flush = getattr(self.decoder, "flush", None)
401
+ if flush is None:
402
+ return
403
+ try:
404
+ tail = flush()
405
+ except Exception as e:
406
+ self.logger.event(f"[!] sync 解压 flush 异常: {e}")
407
+ self.decoder_lib_missing = True
408
+ self.decoder_fallback = "failed"
409
+ return
410
+ if tail:
411
+ self._write_capture(tail)
412
+ self.accumulated += len(tail)
413
+
414
+ def _open_capture(self):
415
+ # 统一命名:sync-{stem}-conn{N}-{size}B-{time_ns}-{seq}{ext}
416
+ # - SEND(push):临时名 …-tmp-…,DONE 后 _close_capture rename 为终名(含真实 accumulated)
417
+ # - RECV(pull):终名直接用 STAT/STA2 size;极少数 stat 未到 → 0B
418
+ # stem sanitize 后最多 64 字符
419
+ if not self.capture_enabled:
420
+ self.capture_path = None
421
+ self.file_fp = None
422
+ return
423
+ try:
424
+ stem, ext = os.path.splitext(self.basename)
425
+ stem = re.sub(r"[^A-Za-z0-9._-]", "_", stem)[:64] or "sync_file"
426
+ stream_id = getattr(self.logger, "conn_id", "na")
427
+ if self.op == "SEND":
428
+ # push 临时名(不打 captured,DONE 后 rename 为终名)
429
+ prefix = f"sync-{stem}-conn{stream_id}-tmp"
430
+ else:
431
+ # pull 终名:size 取 STAT/STA2;未到则 0B(captured 仍报真实 accumulated)
432
+ size = self.remote_size if self.remote_size is not None else 0
433
+ prefix = f"sync-{stem}-conn{stream_id}-{size}B"
434
+ self.capture_path, self.file_fp = open_unique_capture(
435
+ self.files_dir, prefix, ext
436
+ )
437
+ except IOError as e:
438
+ self.logger.event(f"[!] capture open failed: {e}")
439
+ self.capture_path = None
440
+ self.file_fp = None
441
+ self.capture_overflow = True
442
+
443
+ def _write_capture(self, payload):
444
+ """流式写盘:每帧 payload 直接 write 到已 open file_fp。IOError 内化不冒泡。"""
445
+ if not self.file_fp or self.capture_overflow:
446
+ return
447
+ if self.accumulated + len(payload) > MAX_SYNC_FILE:
448
+ self.logger.event(
449
+ f"[!] sync file > {MAX_SYNC_FILE} bytes, stop capturing: {self.basename}"
450
+ )
451
+ self._close_capture()
452
+ self.capture_overflow = True
453
+ return
454
+ try:
455
+ self.file_fp.write(payload)
456
+ except IOError as e:
457
+ self.logger.event(f"[!] capture write failed: {e}")
458
+ self._close_capture()
459
+ self.capture_overflow = True
460
+
461
+ def _close_capture(self, mtime=None):
462
+ # mtime 仅 push DONE 传入(DONE length 字段),用于临时名 rename 为终名后打唯一 captured。
463
+ # pull/FAIL/QUIT/overflow/teardown 均传 None:
464
+ # - pull DONE:终名已在 _open_capture 定,直接打 captured(size=accumulated)
465
+ # - push 未到 DONE(FAIL/QUIT/overflow/teardown):file_fp 可能仍非 None → 守卫放行 close 但不打 captured(临时名不打 captured)
466
+ # - 二次 close(DONE 后 OKAY):file_fp 已 None → 守卫 return,不重复 captured
467
+ # 守卫:--no-capture / disabled / overflow 已 close / post-DONE OKAY 路径,避免 os.replace(None) TypeError
468
+ if not self.file_fp:
469
+ return
470
+ try:
471
+ self.file_fp.close()
472
+ except Exception:
473
+ pass
474
+ self.file_fp = None
475
+ if not (self.capture_enabled and self.capture_path):
476
+ return
477
+ cap_note = f" {self._capture_metadata()}"
478
+ # push 分支:DONE 后 rename 临时名 -> 终名 sync-{stem}-conn{N}-{accumulated}B-…,rename 后打 captured
479
+ if self.op == "SEND" and mtime is not None and self.basename:
480
+ try:
481
+ stem, ext = os.path.splitext(self.basename)
482
+ stem = re.sub(r"[^A-Za-z0-9._-]", "_", stem)[:64] or "sync_file"
483
+ stream_id = getattr(self.logger, "conn_id", "na")
484
+ final_path = reserve_unique_capture_path(
485
+ self.files_dir,
486
+ f"sync-{stem}-conn{stream_id}-{self.accumulated}B",
487
+ ext,
488
+ )
489
+ source_path = self.capture_path
490
+ try:
491
+ os.replace(source_path, final_path)
492
+ except Exception:
493
+ try:
494
+ os.unlink(final_path)
495
+ except OSError:
496
+ pass
497
+ raise
498
+ self.capture_path = final_path # 路径=最终权威
499
+ except Exception as e:
500
+ # rename 失败降级:保留临时名,不阻断转发(转发 sendall 在更早已执行)
501
+ self.logger.event(f"[!] sync rename failed: {e}")
502
+ self.logger.event(
503
+ f"[sync captured]: {self.capture_path} size={self.accumulated} mtime={mtime}{cap_note}"
504
+ )
505
+ elif self.op == "SEND":
506
+ # push 未到 DONE 就 close(overflow/FAIL/QUIT/teardown,mtime is None):
507
+ # 临时名不打 captured(符合"只一条 captured 且路径为最终名")。文件留临时名属降级。
508
+ return
509
+ else:
510
+ # pull 分支(op=="RECV",mtime is None):终名已在 _open_capture 定,打 captured(无 mtime)
511
+ self.logger.event(
512
+ f"[sync captured]: {self.capture_path} size={self.accumulated}{cap_note}"
513
+ )
514
+
515
+ def _reset_file_state(self):
516
+ """多文件:DONE/OKAY 后复位文件状态(frame_buf 保留余量,下一文件继续切帧)。"""
517
+ self.op = None
518
+ self.expecting = "idle"
519
+ self.awaiting_details = None
520
+ self.remote_path = None
521
+ self.basename = None
522
+ self.remote_size = None
523
+ self.accumulated = 0
524
+ self.capture_overflow = False
525
+ # P4: 每文件复位解压器(_start_* 已重置,此处兜底多文件 DONE/OKAY 后场景)
526
+ self.compression = None
527
+ self.decoder = None
528
+ self.decoder_lib_missing = False
529
+ self.decoder_fallback = None
530
+
531
+ def _capture_metadata(self):
532
+ """描述落盘内容格式,明确区分解码成功、缺失和中途失败。"""
533
+ if self.compression is None:
534
+ return "capture_format=raw compression=none decoder=not-needed"
535
+ if self.decoder_fallback == "unavailable":
536
+ return (
537
+ f"capture_format=wire-compressed compression={self.compression} "
538
+ "decoder=unavailable"
539
+ )
540
+ if self.decoder_fallback == "failed":
541
+ return (
542
+ f"capture_format=mixed compression={self.compression} decoder=failed"
543
+ )
544
+ return f"capture_format=decoded compression={self.compression} decoder=ok"
545
+
546
+ def close(self):
547
+ """连接关闭时兜底关闭未收完的文件(保留部分落盘便于排查)。"""
548
+ with self.lock:
549
+ self._close_capture()
550
+ self.expecting = None
@@ -0,0 +1 @@
1
+ """从参考脚本迁移而来的 ADB 线协议工具。"""
@@ -0,0 +1,66 @@
1
+ """AdbProtocol facade: re-exports smart-socket + payload pure codecs.
2
+
3
+ Appendix A symbols stay importable as ``AdbProtocol.<name>`` during Track 1.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from adbproxy.protocols.payload.install import (
9
+ is_install_stream_service as _is_install_stream_service,
10
+ )
11
+ from adbproxy.protocols.payload.install import (
12
+ parse_install_size as _parse_install_size,
13
+ )
14
+ from adbproxy.protocols.payload.shell_v2 import split_v2_frames as _split_v2_frames
15
+ from adbproxy.protocols.smart_socket.classifier import ServiceClassifier
16
+ from adbproxy.protocols.smart_socket.wire import format_service_frame as _format_service_frame
17
+
18
+
19
+ class AdbProtocol:
20
+ """adb client↔server 协议解析工具,全部静态方法、无状态。"""
21
+
22
+ @staticmethod
23
+ def format_service_frame(service):
24
+ """编码 smart-socket ``<hex4><body>``,body 不得超过 0xffff 字节。"""
25
+ return _format_service_frame(service)
26
+
27
+ @staticmethod
28
+ def parse_service(svc):
29
+ """拆 service 串 -> (kind, args_set, cmd)。"""
30
+ return ServiceClassifier.parse_service(svc)
31
+
32
+ @staticmethod
33
+ def is_host_service(svc):
34
+ return ServiceClassifier.is_host_service(svc)
35
+
36
+ @staticmethod
37
+ def host_response_kind(svc):
38
+ """按 AOSP host service 选择响应 wire shape。"""
39
+ return ServiceClassifier.host_response_kind(svc)
40
+
41
+ @staticmethod
42
+ def is_shell_service(svc):
43
+ return ServiceClassifier.is_shell_service(svc)
44
+
45
+ @staticmethod
46
+ def is_sync_service(svc):
47
+ return ServiceClassifier.is_sync_service(svc)
48
+
49
+ @staticmethod
50
+ def is_install_stream_service(svc):
51
+ """判断是否为会发送 raw apk 字节的 install 流式服务。"""
52
+ return _is_install_stream_service(svc)
53
+
54
+ @staticmethod
55
+ def parse_install_size(svc):
56
+ """从 install 流式服务串提取 -S <size>。返回 int 或 None。"""
57
+ return _parse_install_size(svc)
58
+
59
+ @staticmethod
60
+ def split_v2_frames(buf):
61
+ """切分 v2 帧 [1B id][4B LE len][data]。
62
+
63
+ 返回 (frames:list[(id,payload)], leftover:bytearray, ok:bool)。
64
+ ok=False 表示遇到非法 len,调用方应降级为碎片流。
65
+ """
66
+ return _split_v2_frames(buf)
@@ -0,0 +1,17 @@
1
+ """Pure L3 payload codecs (shell_v2 / sync / install). No I/O."""
2
+
3
+ from adbproxy.protocols.payload.install import (
4
+ is_install_stream_service,
5
+ parse_install_size,
6
+ )
7
+ from adbproxy.protocols.payload.shell_v2 import split_v2_frames
8
+ from adbproxy.protocols.payload.sync import SyncFrame, SyncWireDecoder, plan_sync_frame
9
+
10
+ __all__ = [
11
+ "SyncFrame",
12
+ "SyncWireDecoder",
13
+ "is_install_stream_service",
14
+ "parse_install_size",
15
+ "plan_sync_frame",
16
+ "split_v2_frames",
17
+ ]
@@ -0,0 +1,44 @@
1
+ """Pure install stream service grammar (abb_exec package install / install-write)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ # install 流式服务命中正则(核对 AOSP client/adb_install.cpp:208-243,637-643):
8
+ # 仅 package install / install-write 会发送 raw apk 字节;
9
+ # install-create/install-commit/install-abandon 不发文件体,必须排除。
10
+ # abb_exec: 服务的参数以 NULL 字节 \x00 作 ABB_ARG_DELIMETER(见 AOSP
11
+ # client/commandline.h send_abb_exec_command),非空格分隔。故分隔符须
12
+ # 同时匹配空白与 NULL:[\s\x00]。
13
+ _INSTALL_BODY_RE = re.compile(r"[\s\x00]+install(?:-write)?(?:[\s\x00]|$)")
14
+ _INSTALL_SIZE_RE = re.compile(r"-S[\s\x00]+(\d+)")
15
+
16
+
17
+ def is_install_stream_service(svc: str | None) -> bool:
18
+ """判断是否为会发送 raw apk 字节的 install 流式服务。
19
+
20
+ 命中:abb_exec:package install / install-write 且命令体含 -S <size>,
21
+ 排除 install-create/install-commit/install-abandon(仅收发状态行,不发文件体)。
22
+ """
23
+ if svc is None or not svc.startswith("abb_exec:"):
24
+ return False
25
+ body = svc[len("abb_exec:") :]
26
+ if not body.startswith("package"):
27
+ return False
28
+ rest = body[len("package") :]
29
+ # 词边界匹配 install / install-write(install-create 因后续非空白/末尾被排除)
30
+ if not _INSTALL_BODY_RE.match(rest):
31
+ return False
32
+ # 显式排除不发文件体的控制命令(双保险)
33
+ for bad in ("install-create", "install-commit", "install-abandon"):
34
+ if bad in svc:
35
+ return False
36
+ return _INSTALL_SIZE_RE.search(svc) is not None
37
+
38
+
39
+ def parse_install_size(svc: str | None) -> int | None:
40
+ """从 install 流式服务串提取 -S <size>。返回 int 或 None。"""
41
+ if not is_install_stream_service(svc):
42
+ return None
43
+ m = _INSTALL_SIZE_RE.search(svc)
44
+ return int(m.group(1)) if m else None