pymobiledevice3 5.0.4__py3-none-any.whl → 7.0.6__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 (79) hide show
  1. misc/understanding_idevice_protocol_layers.md +10 -5
  2. pymobiledevice3/__main__.py +171 -46
  3. pymobiledevice3/_version.py +2 -2
  4. pymobiledevice3/bonjour.py +22 -21
  5. pymobiledevice3/cli/activation.py +24 -22
  6. pymobiledevice3/cli/afc.py +49 -41
  7. pymobiledevice3/cli/amfi.py +13 -18
  8. pymobiledevice3/cli/apps.py +71 -65
  9. pymobiledevice3/cli/backup.py +134 -93
  10. pymobiledevice3/cli/bonjour.py +31 -29
  11. pymobiledevice3/cli/cli_common.py +175 -232
  12. pymobiledevice3/cli/companion_proxy.py +12 -12
  13. pymobiledevice3/cli/crash.py +95 -52
  14. pymobiledevice3/cli/developer/__init__.py +62 -0
  15. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  16. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  17. pymobiledevice3/cli/developer/arbitration.py +50 -0
  18. pymobiledevice3/cli/developer/condition.py +33 -0
  19. pymobiledevice3/cli/developer/core_device.py +294 -0
  20. pymobiledevice3/cli/developer/debugserver.py +244 -0
  21. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  22. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  23. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  25. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  26. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  27. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  28. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  29. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  30. pymobiledevice3/cli/idam.py +42 -0
  31. pymobiledevice3/cli/lockdown.py +70 -75
  32. pymobiledevice3/cli/mounter.py +99 -57
  33. pymobiledevice3/cli/notification.py +38 -26
  34. pymobiledevice3/cli/pcap.py +36 -20
  35. pymobiledevice3/cli/power_assertion.py +15 -16
  36. pymobiledevice3/cli/processes.py +11 -17
  37. pymobiledevice3/cli/profile.py +120 -75
  38. pymobiledevice3/cli/provision.py +27 -26
  39. pymobiledevice3/cli/remote.py +109 -100
  40. pymobiledevice3/cli/restore.py +134 -129
  41. pymobiledevice3/cli/springboard.py +50 -50
  42. pymobiledevice3/cli/syslog.py +145 -65
  43. pymobiledevice3/cli/usbmux.py +66 -27
  44. pymobiledevice3/cli/version.py +2 -5
  45. pymobiledevice3/cli/webinspector.py +232 -156
  46. pymobiledevice3/exceptions.py +6 -2
  47. pymobiledevice3/lockdown.py +5 -1
  48. pymobiledevice3/lockdown_service_provider.py +5 -0
  49. pymobiledevice3/remote/remote_service_discovery.py +18 -10
  50. pymobiledevice3/restore/device.py +28 -4
  51. pymobiledevice3/restore/restore.py +2 -2
  52. pymobiledevice3/service_connection.py +15 -12
  53. pymobiledevice3/services/afc.py +731 -220
  54. pymobiledevice3/services/device_link.py +45 -31
  55. pymobiledevice3/services/idam.py +20 -0
  56. pymobiledevice3/services/lockdown_service.py +12 -9
  57. pymobiledevice3/services/mobile_config.py +1 -0
  58. pymobiledevice3/services/mobilebackup2.py +6 -3
  59. pymobiledevice3/services/os_trace.py +97 -55
  60. pymobiledevice3/services/remote_fetch_symbols.py +13 -8
  61. pymobiledevice3/services/screenshot.py +2 -2
  62. pymobiledevice3/services/web_protocol/alert.py +8 -8
  63. pymobiledevice3/services/web_protocol/automation_session.py +87 -79
  64. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  65. pymobiledevice3/services/web_protocol/driver.py +71 -70
  66. pymobiledevice3/services/web_protocol/element.py +58 -56
  67. pymobiledevice3/services/web_protocol/selenium_api.py +47 -47
  68. pymobiledevice3/services/web_protocol/session_protocol.py +3 -2
  69. pymobiledevice3/services/web_protocol/switch_to.py +23 -19
  70. pymobiledevice3/services/webinspector.py +42 -67
  71. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +5 -3
  72. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/RECORD +76 -61
  73. pymobiledevice3/cli/completions.py +0 -50
  74. pymobiledevice3/cli/developer.py +0 -1539
  75. pymobiledevice3/cli/diagnostics.py +0 -110
  76. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +0 -0
  77. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  78. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/licenses/LICENSE +0 -0
  79. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env python3
2
+ """
3
+ AFC (Apple File Connection) Service Module
4
+
5
+ This module provides an interface to interact with iOS devices' file systems through the AFC protocol.
6
+ It supports file operations like reading, writing, deleting, and directory traversal, as well as an
7
+ interactive shell for navigating the device's file system rooted at /var/mobile/Media.
8
+ """
2
9
 
3
10
  import logging
4
11
  import os
@@ -11,13 +18,15 @@ import struct
11
18
  import sys
12
19
  import warnings
13
20
  from collections import namedtuple
21
+ from dataclasses import dataclass, field
14
22
  from datetime import datetime
15
23
  from re import Pattern
16
24
  from typing import Callable, Optional, Union
17
25
 
18
26
  import hexdump
19
27
  from click.exceptions import Exit
20
- from construct import Const, Container, CString, Enum, GreedyRange, Int64ul, Struct, Tell
28
+ from construct import Const, CString, GreedyRange, Int64ul, Tell
29
+ from construct_typed import DataclassMixin, EnumBase, TEnum, TStruct, csfield
21
30
  from parameter_decorators import path_to_str
22
31
  from pygments import formatters, highlight, lexers
23
32
  from pygnuutils.cli.ls import ls as ls_cli
@@ -56,185 +65,239 @@ StatResult = namedtuple(
56
65
  ],
57
66
  )
58
67
 
59
- afc_opcode_t = Enum(
60
- Int64ul,
61
- STATUS=0x00000001,
62
- DATA=0x00000002, # Data */
63
- READ_DIR=0x00000003, # ReadDir */
64
- READ_FILE=0x00000004, # ReadFile */
65
- WRITE_FILE=0x00000005, # WriteFile */
66
- WRITE_PART=0x00000006, # WritePart */
67
- TRUNCATE=0x00000007, # TruncateFile */
68
- REMOVE_PATH=0x00000008, # RemovePath */
69
- MAKE_DIR=0x00000009, # MakeDir */
70
- GET_FILE_INFO=0x0000000A, # GetFileInfo */
71
- GET_DEVINFO=0x0000000B, # GetDeviceInfo */
72
- WRITE_FILE_ATOM=0x0000000C, # WriteFileAtomic (tmp file+rename) */
73
- FILE_OPEN=0x0000000D, # FileRefOpen */
74
- FILE_OPEN_RES=0x0000000E, # FileRefOpenResult */
75
- READ=0x0000000F, # FileRefRead */
76
- WRITE=0x00000010, # FileRefWrite */
77
- FILE_SEEK=0x00000011, # FileRefSeek */
78
- FILE_TELL=0x00000012, # FileRefTell */
79
- FILE_TELL_RES=0x00000013, # FileRefTellResult */
80
- FILE_CLOSE=0x00000014, # FileRefClose */
81
- FILE_SET_SIZE=0x00000015, # FileRefSetFileSize (ftruncate) */
82
- GET_CON_INFO=0x00000016, # GetConnectionInfo */
83
- SET_CON_OPTIONS=0x00000017, # SetConnectionOptions */
84
- RENAME_PATH=0x00000018, # RenamePath */
85
- SET_FS_BS=0x00000019, # SetFSBlockSize (0x800000) */
86
- SET_SOCKET_BS=0x0000001A, # SetSocketBlockSize (0x800000) */
87
- FILE_LOCK=0x0000001B, # FileRefLock */
88
- MAKE_LINK=0x0000001C, # MakeLink */
89
- SET_FILE_TIME=0x0000001E, # set st_mtime */
90
- )
91
-
92
- afc_error_t = Enum(
93
- Int64ul,
94
- SUCCESS=0,
95
- UNKNOWN_ERROR=1,
96
- OP_HEADER_INVALID=2,
97
- NO_RESOURCES=3,
98
- READ_ERROR=4,
99
- WRITE_ERROR=5,
100
- UNKNOWN_PACKET_TYPE=6,
101
- INVALID_ARG=7,
102
- OBJECT_NOT_FOUND=8,
103
- OBJECT_IS_DIR=9,
104
- PERM_DENIED=10,
105
- SERVICE_NOT_CONNECTED=11,
106
- OP_TIMEOUT=12,
107
- TOO_MUCH_DATA=13,
108
- END_OF_DATA=14,
109
- OP_NOT_SUPPORTED=15,
110
- OBJECT_EXISTS=16,
111
- OBJECT_BUSY=17,
112
- NO_SPACE_LEFT=18,
113
- OP_WOULD_BLOCK=19,
114
- IO_ERROR=20,
115
- OP_INTERRUPTED=21,
116
- OP_IN_PROGRESS=22,
117
- INTERNAL_ERROR=23,
118
- MUX_ERROR=30,
119
- NO_MEM=31,
120
- NOT_ENOUGH_DATA=32,
121
- DIR_NOT_EMPTY=33,
122
- )
123
-
124
- afc_link_type_t = Enum(
125
- Int64ul,
126
- HARDLINK=1,
127
- SYMLINK=2,
128
- )
129
68
 
130
- afc_fopen_mode_t = Enum(
131
- Int64ul,
132
- RDONLY=0x00000001, # /**< r O_RDONLY */
133
- RW=0x00000002, # /**< r+ O_RDWR | O_CREAT */
134
- WRONLY=0x00000003, # /**< w O_WRONLY | O_CREAT | O_TRUNC */
135
- WR=0x00000004, # /**< w+ O_RDWR | O_CREAT | O_TRUNC */
136
- APPEND=0x00000005, # /**< a O_WRONLY | O_APPEND | O_CREAT */
137
- RDAPPEND=0x00000006, # /**< a+ O_RDWR | O_APPEND | O_CREAT */
138
- )
69
+ class AfcOpcode(EnumBase):
70
+ STATUS = 0x00000001
71
+ DATA = 0x00000002 # Data
72
+ READ_DIR = 0x00000003 # ReadDir
73
+ READ_FILE = 0x00000004 # ReadFile
74
+ WRITE_FILE = 0x00000005 # WriteFile
75
+ WRITE_PART = 0x00000006 # WritePart
76
+ TRUNCATE = 0x00000007 # TruncateFile
77
+ REMOVE_PATH = 0x00000008 # RemovePath
78
+ MAKE_DIR = 0x00000009 # MakeDir
79
+ GET_FILE_INFO = 0x0000000A # GetFileInfo
80
+ GET_DEVINFO = 0x0000000B # GetDeviceInfo
81
+ WRITE_FILE_ATOM = 0x0000000C # WriteFileAtomic (tmp file+rename)
82
+ FILE_OPEN = 0x0000000D # FileRefOpen
83
+ FILE_OPEN_RES = 0x0000000E # FileRefOpenResult
84
+ READ = 0x0000000F # FileRefRead
85
+ WRITE = 0x00000010 # FileRefWrite
86
+ FILE_SEEK = 0x00000011 # FileRefSeek
87
+ FILE_TELL = 0x00000012 # FileRefTell
88
+ FILE_TELL_RES = 0x00000013 # FileRefTellResult
89
+ FILE_CLOSE = 0x00000014 # FileRefClose
90
+ FILE_SET_SIZE = 0x00000015 # FileRefSetFileSize (ftruncate)
91
+ GET_CON_INFO = 0x00000016 # GetConnectionInfo
92
+ SET_CON_OPTIONS = 0x00000017 # SetConnectionOptions
93
+ RENAME_PATH = 0x00000018 # RenamePath
94
+ SET_FS_BS = 0x00000019 # SetFSBlockSize (0x800000)
95
+ SET_SOCKET_BS = 0x0000001A # SetSocketBlockSize (0x800000)
96
+ FILE_LOCK = 0x0000001B # FileRefLock
97
+ MAKE_LINK = 0x0000001C # MakeLink
98
+ SET_FILE_TIME = 0x0000001E # set st_mtime
99
+
100
+
101
+ class AfcError(EnumBase):
102
+ SUCCESS = 0
103
+ UNKNOWN_ERROR = 1
104
+ OP_HEADER_INVALID = 2
105
+ NO_RESOURCES = 3
106
+ READ_ERROR = 4
107
+ WRITE_ERROR = 5
108
+ UNKNOWN_PACKET_TYPE = 6
109
+ INVALID_ARG = 7
110
+ OBJECT_NOT_FOUND = 8
111
+ OBJECT_IS_DIR = 9
112
+ PERM_DENIED = 10
113
+ SERVICE_NOT_CONNECTED = 11
114
+ OP_TIMEOUT = 12
115
+ TOO_MUCH_DATA = 13
116
+ END_OF_DATA = 14
117
+ OP_NOT_SUPPORTED = 15
118
+ OBJECT_EXISTS = 16
119
+ OBJECT_BUSY = 17
120
+ NO_SPACE_LEFT = 18
121
+ OP_WOULD_BLOCK = 19
122
+ IO_ERROR = 20
123
+ OP_INTERRUPTED = 21
124
+ OP_IN_PROGRESS = 22
125
+ INTERNAL_ERROR = 23
126
+ MUX_ERROR = 30
127
+ NO_MEM = 31
128
+ NOT_ENOUGH_DATA = 32
129
+ DIR_NOT_EMPTY = 33
130
+
131
+
132
+ class AfcLinkType(EnumBase):
133
+ HARDLINK = 1
134
+ SYMLINK = 2
135
+
136
+
137
+ class AfcFopenMode(EnumBase):
138
+ RDONLY = 0x00000001 # r O_RDONLY
139
+ RW = 0x00000002 # r+ O_RDWR | O_CREAT
140
+ WRONLY = 0x00000003 # w O_WRONLY | O_CREAT | O_TRUNC
141
+ WR = 0x00000004 # w+ O_RDWR | O_CREAT | O_TRUNC
142
+ APPEND = 0x00000005 # a O_WRONLY | O_APPEND | O_CREAT
143
+ RDAPPEND = 0x00000006 # a+ O_RDWR | O_APPEND | O_CREAT
144
+
145
+
146
+ # typed construct adapters for the enums
147
+ afc_opcode_t = TEnum(Int64ul, AfcOpcode)
148
+ afc_error_construct = TEnum(Int64ul, AfcError)
149
+ afc_link_type_construct = TEnum(Int64ul, AfcLinkType)
150
+ afc_fopen_mode_construct = TEnum(Int64ul, AfcFopenMode)
139
151
 
140
152
  AFC_FOPEN_TEXTUAL_MODES = {
141
- "r": afc_fopen_mode_t.RDONLY,
142
- "r+": afc_fopen_mode_t.RW,
143
- "w": afc_fopen_mode_t.WRONLY,
144
- "w+": afc_fopen_mode_t.WR,
145
- "a": afc_fopen_mode_t.APPEND,
146
- "a+": afc_fopen_mode_t.RDAPPEND,
153
+ "r": AfcFopenMode.RDONLY,
154
+ "r+": AfcFopenMode.RW,
155
+ "w": AfcFopenMode.WRONLY,
156
+ "w+": AfcFopenMode.WR,
157
+ "a": AfcFopenMode.APPEND,
158
+ "a+": AfcFopenMode.RDAPPEND,
147
159
  }
148
160
 
149
- AFC_LOCK_SH = 1 | 4 # /**< shared lock */
150
- AFC_LOCK_EX = 2 | 4 # /**< exclusive lock */
151
- AFC_LOCK_UN = 8 | 4 # /**< unlock */
161
+ AFC_LOCK_SH = 1 | 4 # shared lock
162
+ AFC_LOCK_EX = 2 | 4 # exclusive lock
163
+ AFC_LOCK_UN = 8 | 4 # unlock
152
164
 
153
165
  MAXIMUM_WRITE_SIZE = 1 << 30
154
166
 
155
167
  AFCMAGIC = b"CFA6LPAA"
156
168
 
157
- afc_header_t = Struct(
158
- "magic" / Const(AFCMAGIC),
159
- "entire_length" / Int64ul,
160
- "this_length" / Int64ul,
161
- "packet_num" / Int64ul,
162
- "operation" / afc_opcode_t,
163
- "_data_offset" / Tell,
164
- )
165
169
 
166
- afc_read_dir_req_t = Struct(
167
- "filename" / CString("utf8"),
168
- )
170
+ @dataclass
171
+ class AfcHeader(DataclassMixin):
172
+ magic: bytes = csfield(Const(AFCMAGIC))
173
+ entire_length: int = csfield(Int64ul)
174
+ this_length: int = csfield(Int64ul)
175
+ packet_num: int = csfield(Int64ul)
176
+ operation: AfcOpcode = csfield(afc_opcode_t)
177
+ _data_offset: int = field(default=0, init=False, metadata={"subcon": Tell})
169
178
 
170
- afc_read_dir_resp_t = Struct(
171
- "filenames" / GreedyRange(CString("utf8")),
172
- )
173
179
 
174
- afc_mkdir_req_t = Struct(
175
- "filename" / CString("utf8"),
176
- )
180
+ @dataclass
181
+ class AfcReadDirRequest(DataclassMixin):
182
+ filename: str = csfield(CString("utf8"))
177
183
 
178
- afc_stat_t = Struct(
179
- "filename" / CString("utf8"),
180
- )
181
184
 
182
- afc_make_link_req_t = Struct(
183
- "type" / afc_link_type_t,
184
- "target" / CString("utf8"),
185
- "source" / CString("utf8"),
186
- )
185
+ @dataclass
186
+ class AfcReadDirResponse(DataclassMixin):
187
+ filenames: list[str] = csfield(GreedyRange(CString("utf8")))
187
188
 
188
- afc_fopen_req_t = Struct(
189
- "mode" / afc_fopen_mode_t,
190
- "filename" / CString("utf8"),
191
- )
192
189
 
193
- afc_fopen_resp_t = Struct(
194
- "handle" / Int64ul,
195
- )
190
+ @dataclass
191
+ class AfcMkdirRequest(DataclassMixin):
192
+ filename: str = csfield(CString("utf8"))
196
193
 
197
- afc_fclose_req_t = Struct(
198
- "handle" / Int64ul,
199
- )
200
194
 
201
- afc_rm_req_t = Struct(
202
- "filename" / CString("utf8"),
203
- )
195
+ @dataclass
196
+ class AfcStatRequest(DataclassMixin):
197
+ filename: str = csfield(CString("utf8"))
204
198
 
205
- afc_rename_req_t = Struct(
206
- "source" / CString("utf8"),
207
- "target" / CString("utf8"),
208
- )
209
199
 
210
- afc_fread_req_t = Struct(
211
- "handle" / Int64ul,
212
- "size" / Int64ul,
213
- )
200
+ @dataclass
201
+ class AfcMakeLinkRequest(DataclassMixin):
202
+ type: AfcLinkType = csfield(afc_link_type_construct)
203
+ target: str = csfield(CString("utf8"))
204
+ source: str = csfield(CString("utf8"))
205
+
206
+
207
+ @dataclass
208
+ class AfcFopenRequest(DataclassMixin):
209
+ mode: AfcFopenMode = csfield(afc_fopen_mode_construct)
210
+ filename: str = csfield(CString("utf8"))
211
+
212
+
213
+ @dataclass
214
+ class AfcFopenResponse(DataclassMixin):
215
+ handle: int = csfield(Int64ul)
216
+
217
+
218
+ @dataclass
219
+ class AfcFcloseRequest(DataclassMixin):
220
+ handle: int = csfield(Int64ul)
221
+
222
+
223
+ @dataclass
224
+ class AfcRmRequest(DataclassMixin):
225
+ filename: str = csfield(CString("utf8"))
226
+
227
+
228
+ @dataclass
229
+ class AfcRenameRequest(DataclassMixin):
230
+ source: str = csfield(CString("utf8"))
231
+ target: str = csfield(CString("utf8"))
214
232
 
215
- afc_lock_t = Struct(
216
- "handle" / Int64ul,
217
- "op" / Int64ul,
218
- )
219
233
 
234
+ @dataclass
235
+ class AfcFreadRequest(DataclassMixin):
236
+ handle: int = csfield(Int64ul)
237
+ size: int = csfield(Int64ul)
220
238
 
221
- def list_to_dict(d):
222
- d = d.decode("utf-8")
223
- t = d.split("\x00")
224
- t = t[:-1]
225
239
 
226
- assert len(t) % 2 == 0
227
- res = {}
228
- for i in range(int(len(t) / 2)):
229
- res[t[i * 2]] = t[i * 2 + 1]
230
- return res
240
+ @dataclass
241
+ class AfcLockRequest(DataclassMixin):
242
+ handle: int = csfield(Int64ul)
243
+ op: int = csfield(Int64ul)
244
+
245
+
246
+ afc_header_t = TStruct(AfcHeader)
247
+ afc_read_dir_req_t = TStruct(AfcReadDirRequest)
248
+ afc_read_dir_resp_t = TStruct(AfcReadDirResponse)
249
+ afc_mkdir_req_t = TStruct(AfcMkdirRequest)
250
+ afc_stat_t = TStruct(AfcStatRequest)
251
+ afc_make_link_req_t = TStruct(AfcMakeLinkRequest)
252
+ afc_fopen_req_t = TStruct(AfcFopenRequest)
253
+ afc_fopen_resp_t = TStruct(AfcFopenResponse)
254
+ afc_fclose_req_t = TStruct(AfcFcloseRequest)
255
+ afc_rm_req_t = TStruct(AfcRmRequest)
256
+ afc_rename_req_t = TStruct(AfcRenameRequest)
257
+ afc_fread_req_t = TStruct(AfcFreadRequest)
258
+ afc_lock_t = TStruct(AfcLockRequest)
259
+
260
+
261
+ def list_to_dict(raw: bytes) -> dict[str, str]:
262
+ """
263
+ Convert a null-terminated key-value list to a dictionary.
264
+
265
+ The input is expected to be a byte string with alternating keys and values,
266
+ each separated by null bytes (``\\x00``).
267
+ """
268
+ parts = raw.decode("utf-8").split("\x00")[:-1]
269
+ if len(parts) % 2:
270
+ raise ValueError("AFC list is not key/value aligned")
271
+ return dict(zip(parts[::2], parts[1::2]))
231
272
 
232
273
 
233
274
  class AfcService(LockdownService):
275
+ """
276
+ Apple File Connection (AFC) Service for iOS device file system access.
277
+
278
+ This service provides full file system access to the /var/mobile/Media directory
279
+ on iOS devices. It supports standard file operations including read, write, delete,
280
+ rename, and directory operations.
281
+
282
+ The service communicates using a custom binary protocol with operation codes for
283
+ different file system operations.
284
+
285
+ Attributes:
286
+ SERVICE_NAME: Service identifier for lockdown-based connections
287
+ RSD_SERVICE_NAME: Service identifier for RSD-based connections
288
+ packet_num: Counter for tracking packet sequence numbers
289
+ """
290
+
234
291
  SERVICE_NAME = "com.apple.afc"
235
292
  RSD_SERVICE_NAME = "com.apple.afc.shim.remote"
236
293
 
237
294
  def __init__(self, lockdown: LockdownServiceProvider, service_name: Optional[str] = None):
295
+ """
296
+ Initialize the AFC service.
297
+
298
+ :param lockdown: Lockdown service provider for establishing connection
299
+ :param service_name: Optional service name override. Auto-detected if None
300
+ """
238
301
  if service_name is None:
239
302
  service_name = self.SERVICE_NAME if isinstance(lockdown, LockdownClient) else self.RSD_SERVICE_NAME
240
303
  super().__init__(lockdown, service_name)
@@ -250,6 +313,20 @@ class AfcService(LockdownService):
250
313
  ignore_errors: bool = False,
251
314
  progress_bar: bool = True,
252
315
  ) -> None:
316
+ """
317
+ Pull (download) a file or directory from the device to the local machine.
318
+
319
+ Recursively copies files and directories from the device to the local file system.
320
+ Preserves modification times and handles large files by reading in chunks.
321
+
322
+ :param relative_src: Source path relative to src_dir on the device
323
+ :param dst: Destination path on the local machine
324
+ :param match: Optional regex pattern to filter files (by basename)
325
+ :param callback: Optional callback function called for each file copied (src, dst)
326
+ :param src_dir: Base directory for resolving relative_src
327
+ :param ignore_errors: If True, continue on errors instead of raising exceptions
328
+ :param progress_bar: If True, show progress bar for large files
329
+ """
253
330
  src = self.resolve_path(posixpath.join(src_dir, relative_src))
254
331
 
255
332
  if not self.isdir(src):
@@ -261,15 +338,13 @@ class AfcService(LockdownService):
261
338
  if src_size <= MAXIMUM_READ_SIZE:
262
339
  f.write(self.get_file_contents(src))
263
340
  else:
264
- left_size = src_size
265
341
  handle = self.fopen(src)
342
+ iterator = range(0, src_size, MAXIMUM_READ_SIZE)
266
343
  if progress_bar:
267
- pb = trange(src_size // MAXIMUM_READ_SIZE + 1)
268
- else:
269
- pb = range(src_size // MAXIMUM_READ_SIZE + 1)
270
- for _ in pb:
271
- f.write(self.fread(handle, min(MAXIMUM_READ_SIZE, left_size)))
272
- left_size -= MAXIMUM_READ_SIZE
344
+ iterator = trange(0, src_size, MAXIMUM_READ_SIZE)
345
+ for offset in iterator:
346
+ to_read = min(MAXIMUM_READ_SIZE, src_size - offset)
347
+ f.write(self.fread(handle, to_read))
273
348
  self.fclose(handle)
274
349
  os.utime(dst, (os.stat(dst).st_atime, self.stat(src)["st_mtime"].timestamp()))
275
350
  if callback is not None:
@@ -315,6 +390,12 @@ class AfcService(LockdownService):
315
390
 
316
391
  @path_to_str()
317
392
  def exists(self, filename):
393
+ """
394
+ Check if a file or directory exists on the device.
395
+
396
+ :param filename: Path to check
397
+ :return: True if the path exists, False otherwise
398
+ """
318
399
  try:
319
400
  self.stat(filename)
320
401
  except AfcFileNotFoundError:
@@ -323,11 +404,26 @@ class AfcService(LockdownService):
323
404
 
324
405
  @path_to_str()
325
406
  def wait_exists(self, filename):
407
+ """
408
+ Block until a file or directory exists on the device.
409
+
410
+ Continuously polls the device until the specified path exists.
411
+ Warning: This is a busy-wait and may consume significant resources.
412
+
413
+ :param filename: Path to wait for
414
+ """
326
415
  while not self.exists(filename):
327
416
  pass
328
417
 
329
418
  @path_to_str()
330
419
  def _push_internal(self, local_path, remote_path, callback=None):
420
+ """
421
+ Internal method for pushing files to the device.
422
+
423
+ :param local_path: Local file or directory path
424
+ :param remote_path: Remote destination path on the device
425
+ :param callback: Optional callback function called for each file copied (src, dst)
426
+ """
331
427
  if callback is not None:
332
428
  callback(local_path, remote_path)
333
429
 
@@ -364,6 +460,16 @@ class AfcService(LockdownService):
364
460
 
365
461
  @path_to_str()
366
462
  def push(self, local_path, remote_path, callback=None):
463
+ """
464
+ Push (upload) a file or directory from the local machine to the device.
465
+
466
+ Recursively copies files and directories from the local file system to the device.
467
+ Creates necessary parent directories if they don't exist.
468
+
469
+ :param local_path: Source path on the local machine
470
+ :param remote_path: Destination path on the device
471
+ :param callback: Optional callback function called for each file copied (src, dst)
472
+ """
367
473
  if os.path.isdir(local_path):
368
474
  remote_path = posixpath.join(remote_path, os.path.basename(local_path))
369
475
  self._push_internal(local_path, remote_path, callback)
@@ -380,7 +486,7 @@ class AfcService(LockdownService):
380
486
  :rtype: bool
381
487
  """
382
488
  try:
383
- self._do_operation(afc_opcode_t.REMOVE_PATH, afc_rm_req_t.build({"filename": filename}))
489
+ self._do_operation(AfcOpcode.REMOVE_PATH, afc_rm_req_t.build(AfcRmRequest(filename=filename)), filename)
384
490
  except AfcException:
385
491
  if force:
386
492
  return False
@@ -440,30 +546,69 @@ class AfcService(LockdownService):
440
546
  return []
441
547
 
442
548
  def get_device_info(self):
443
- return list_to_dict(self._do_operation(afc_opcode_t.GET_DEVINFO))
549
+ """
550
+ Get device file system information.
551
+
552
+ Returns information about the device's file system such as total capacity,
553
+ free space, and block size.
554
+
555
+ :return: Dictionary containing device file system information
556
+ """
557
+ return list_to_dict(self._do_operation(AfcOpcode.GET_DEVINFO))
444
558
 
445
559
  @path_to_str()
446
560
  def listdir(self, filename: str):
447
- data = self._do_operation(afc_opcode_t.READ_DIR, afc_read_dir_req_t.build({"filename": filename}))
561
+ """
562
+ List contents of a directory on the device.
563
+
564
+ :param filename: Path to the directory
565
+ :return: List of filenames in the directory (excluding '.' and '..')
566
+ :raises: AfcException if the path is not a directory or doesn't exist
567
+ """
568
+ data = self._do_operation(AfcOpcode.READ_DIR, afc_read_dir_req_t.build(AfcReadDirRequest(filename=filename)))
448
569
  return afc_read_dir_resp_t.parse(data).filenames[2:] # skip the . and ..
449
570
 
450
571
  @path_to_str()
451
572
  def makedirs(self, filename: str):
452
- return self._do_operation(afc_opcode_t.MAKE_DIR, afc_mkdir_req_t.build({"filename": filename}))
573
+ """
574
+ Create a directory on the device.
575
+
576
+ Note: This behaves like os.makedirs and will create parent directories as needed.
577
+ It is idempotent and will not raise an error if the directory already exists.
578
+
579
+ :param filename: Path of the directory to create
580
+ :return: Response data from the operation
581
+ """
582
+ return self._do_operation(AfcOpcode.MAKE_DIR, afc_mkdir_req_t.build(AfcMkdirRequest(filename=filename)))
453
583
 
454
584
  @path_to_str()
455
585
  def isdir(self, filename: str) -> bool:
586
+ """
587
+ Check if a path is a directory.
588
+
589
+ :param filename: Path to check
590
+ :return: True if the path is a directory, False otherwise
591
+ """
456
592
  stat = self.stat(filename)
457
593
  return stat.get("st_ifmt") == "S_IFDIR"
458
594
 
459
595
  @path_to_str()
460
596
  def stat(self, filename: str):
597
+ """
598
+ Get file or directory statistics.
599
+
600
+ :param filename: Path to the file or directory
601
+ :return: Dictionary containing file statistics (size, mode, mtime, etc.)
602
+ :raises: AfcFileNotFoundError if the path doesn't exist
603
+ """
461
604
  try:
462
605
  stat = list_to_dict(
463
- self._do_operation(afc_opcode_t.GET_FILE_INFO, afc_stat_t.build({"filename": filename}))
606
+ self._do_operation(
607
+ AfcOpcode.GET_FILE_INFO, afc_stat_t.build(AfcStatRequest(filename=filename)), filename
608
+ )
464
609
  )
465
610
  except AfcException as e:
466
- if e.status != afc_error_t.READ_ERROR:
611
+ if e.status != AfcError.READ_ERROR:
467
612
  raise
468
613
  raise AfcFileNotFoundError(e.args[0], e.status) from e
469
614
 
@@ -478,6 +623,15 @@ class AfcService(LockdownService):
478
623
 
479
624
  @path_to_str()
480
625
  def os_stat(self, path: str):
626
+ """
627
+ Get file statistics in os.stat format.
628
+
629
+ Returns a StatResult namedtuple compatible with os.stat results,
630
+ suitable for use with standard Python file handling code.
631
+
632
+ :param path: Path to the file or directory
633
+ :return: StatResult namedtuple with file statistics
634
+ """
481
635
  stat = self.stat(path)
482
636
  mode = 0
483
637
  for s_mode in ["S_IFDIR", "S_IFCHR", "S_IFBLK", "S_IFREG", "S_IFIFO", "S_IFLNK", "S_IFSOCK"]:
@@ -500,72 +654,133 @@ class AfcService(LockdownService):
500
654
  )
501
655
 
502
656
  @path_to_str()
503
- def link(self, target: str, source: str, type_=afc_link_type_t.SYMLINK):
657
+ def link(self, target: str, source: str, type_=AfcLinkType.SYMLINK):
658
+ """
659
+ Create a symbolic or hard link on the device.
660
+
661
+ :param target: The target path that the link will point to
662
+ :param source: The path where the link will be created
663
+ :param type_: Link type (SYMLINK or HARDLINK)
664
+ :return: Response data from the operation
665
+ """
504
666
  return self._do_operation(
505
- afc_opcode_t.MAKE_LINK, afc_make_link_req_t.build({"type": type_, "target": target, "source": source})
667
+ AfcOpcode.MAKE_LINK,
668
+ afc_make_link_req_t.build(AfcMakeLinkRequest(type=type_, target=target, source=source)),
506
669
  )
507
670
 
508
671
  @path_to_str()
509
672
  def fopen(self, filename: str, mode: str = "r") -> int:
673
+ """
674
+ Open a file on the device and return a file handle.
675
+
676
+ :param filename: Path to the file
677
+ :param mode: Open mode ('r', 'r+', 'w', 'w+', 'a', 'a+')
678
+ :return: Integer file handle for subsequent operations
679
+ :raises: ArgumentError if mode is invalid
680
+ """
510
681
  if mode not in AFC_FOPEN_TEXTUAL_MODES:
511
682
  raise ArgumentError(f"mode can be only one of: {AFC_FOPEN_TEXTUAL_MODES.keys()}")
512
683
 
513
684
  data = self._do_operation(
514
- afc_opcode_t.FILE_OPEN, afc_fopen_req_t.build({"mode": AFC_FOPEN_TEXTUAL_MODES[mode], "filename": filename})
685
+ AfcOpcode.FILE_OPEN,
686
+ afc_fopen_req_t.build(
687
+ AfcFopenRequest(mode=AFC_FOPEN_TEXTUAL_MODES[mode], filename=filename),
688
+ ),
515
689
  )
516
690
  return afc_fopen_resp_t.parse(data).handle
517
691
 
518
692
  def fclose(self, handle: int):
519
- return self._do_operation(afc_opcode_t.FILE_CLOSE, afc_fclose_req_t.build({"handle": handle}))
693
+ """
694
+ Close an open file handle.
695
+
696
+ :param handle: File handle returned from fopen
697
+ :return: Response data from the operation
698
+ """
699
+ return self._do_operation(AfcOpcode.FILE_CLOSE, afc_fclose_req_t.build(AfcFcloseRequest(handle=handle)))
520
700
 
521
701
  @path_to_str()
522
- def rename(self, source: str, target: str):
702
+ def rename(self, source: str, target: str) -> None:
703
+ """
704
+ Rename or move a file or directory on the device.
705
+
706
+ :param source: Current path of the file or directory
707
+ :param target: New path for the file or directory
708
+ :raises: AfcFileNotFoundError if source doesn't exist
709
+ """
523
710
  try:
524
- return self._do_operation(
525
- afc_opcode_t.RENAME_PATH, afc_rename_req_t.build({"source": source, "target": target})
711
+ self._do_operation(
712
+ AfcOpcode.RENAME_PATH,
713
+ afc_rename_req_t.build(AfcRenameRequest(source=source, target=target)),
526
714
  )
527
715
  except AfcException as e:
528
716
  if self.exists(source):
529
717
  raise
530
- raise AfcFileNotFoundError(e.args[0], e.status) from e
718
+ raise AfcFileNotFoundError(
719
+ f"Failed to rename {source} into {target}. Got status: {e.status}", e.args[0], str(e.status)
720
+ ) from e
531
721
 
532
- def fread(self, handle: int, sz: bytes) -> bytes:
722
+ def fread(self, handle: int, sz: int) -> bytes:
723
+ """
724
+ Read data from an open file handle.
725
+
726
+ Automatically handles large reads by splitting into multiple operations.
727
+
728
+ :param handle: File handle returned from fopen
729
+ :param sz: Number of bytes to read
730
+ :return: Bytes read from the file
731
+ :raises: AfcException if read operation fails
732
+ """
533
733
  data = b""
534
734
  while sz > 0:
535
735
  to_read = MAXIMUM_READ_SIZE if sz > MAXIMUM_READ_SIZE else sz
536
- self._dispatch_packet(afc_opcode_t.READ, afc_fread_req_t.build({"handle": handle, "size": to_read}))
736
+ self._dispatch_packet(AfcOpcode.READ, afc_fread_req_t.build(AfcFreadRequest(handle=handle, size=to_read)))
537
737
  status, chunk = self._receive_data()
538
- if status != afc_error_t.SUCCESS:
738
+ if status != AfcError.SUCCESS:
539
739
  raise AfcException("fread error", status)
540
740
  sz -= to_read
541
741
  data += chunk
542
742
  return data
543
743
 
544
- def fwrite(self, handle, data, chunk_size=MAXIMUM_WRITE_SIZE):
744
+ def fwrite(self, handle: int, data: bytes, chunk_size: int = MAXIMUM_WRITE_SIZE) -> None:
745
+ """
746
+ Write data to an open file handle.
747
+
748
+ Automatically handles large writes by splitting into multiple operations.
749
+
750
+ :param handle: File handle returned from fopen
751
+ :param data: Bytes to write
752
+ :param chunk_size: Size of each write chunk (default: MAXIMUM_WRITE_SIZE)
753
+ :raises: AfcException if write operation fails
754
+ """
545
755
  file_handle = struct.pack("<Q", handle)
546
756
  chunks_count = len(data) // chunk_size
547
- b = b""
548
757
  for i in range(chunks_count):
549
758
  chunk = data[i * chunk_size : (i + 1) * chunk_size]
550
- self._dispatch_packet(afc_opcode_t.WRITE, file_handle + chunk, this_length=48)
551
- b += chunk
759
+ self._dispatch_packet(AfcOpcode.WRITE, file_handle + chunk, this_length=48)
552
760
 
553
761
  status, _response = self._receive_data()
554
- if status != afc_error_t.SUCCESS:
762
+ if status != AfcError.SUCCESS:
555
763
  raise AfcException(f"failed to write chunk: {status}", status)
556
764
 
557
765
  if len(data) % chunk_size:
558
766
  chunk = data[chunks_count * chunk_size :]
559
- self._dispatch_packet(afc_opcode_t.WRITE, file_handle + chunk, this_length=48)
560
-
561
- b += chunk
767
+ self._dispatch_packet(AfcOpcode.WRITE, file_handle + chunk, this_length=48)
562
768
 
563
769
  status, _response = self._receive_data()
564
- if status != afc_error_t.SUCCESS:
770
+ if status != AfcError.SUCCESS:
565
771
  raise AfcException(f"failed to write last chunk: {status}", status)
566
772
 
567
773
  @path_to_str()
568
774
  def resolve_path(self, filename: str):
775
+ """
776
+ Resolve symbolic links to their target paths.
777
+
778
+ If the path is a symbolic link, returns the target path. Otherwise,
779
+ returns the path unchanged.
780
+
781
+ :param filename: Path to resolve
782
+ :return: Resolved path (or original path if not a symlink)
783
+ """
569
784
  info = self.stat(filename)
570
785
  if info["st_ifmt"] == "S_IFLNK":
571
786
  target = info["LinkTarget"]
@@ -574,11 +789,20 @@ class AfcService(LockdownService):
574
789
 
575
790
  @path_to_str()
576
791
  def get_file_contents(self, filename):
792
+ """
793
+ Read and return the entire contents of a file.
794
+
795
+ Convenience method that opens a file, reads all its contents, and closes it.
796
+
797
+ :param filename: Path to the file
798
+ :return: Bytes containing the file contents
799
+ :raises: AfcException if the path is not a regular file
800
+ """
577
801
  filename = self.resolve_path(filename)
578
802
  info = self.stat(filename)
579
803
 
580
804
  if info["st_ifmt"] != "S_IFREG":
581
- raise AfcException(f"{filename} isn't a file", afc_error_t.INVALID_ARG)
805
+ raise AfcException(f"{filename} isn't a file", AfcError.INVALID_ARG)
582
806
 
583
807
  h = self.fopen(filename)
584
808
  if not h:
@@ -589,12 +813,29 @@ class AfcService(LockdownService):
589
813
 
590
814
  @path_to_str()
591
815
  def set_file_contents(self, filename: str, data: bytes) -> None:
816
+ """
817
+ Write data to a file, creating or overwriting it.
818
+
819
+ Convenience method that opens a file in write mode, writes data, and closes it.
820
+
821
+ :param filename: Path to the file
822
+ :param data: Bytes to write to the file
823
+ """
592
824
  h = self.fopen(filename, "w")
593
825
  self.fwrite(h, data)
594
826
  self.fclose(h)
595
827
 
596
828
  @path_to_str()
597
829
  def walk(self, dirname: str):
830
+ """
831
+ Walk a directory tree, similar to os.walk.
832
+
833
+ Generates tuples of (dirpath, dirnames, filenames) for each directory
834
+ in the tree, starting from dirname.
835
+
836
+ :param dirname: Root directory to walk
837
+ :yields: Tuples of (dirpath, dirnames, filenames)
838
+ """
598
839
  dirs = []
599
840
  files = []
600
841
  for fd in self.listdir(dirname):
@@ -614,6 +855,13 @@ class AfcService(LockdownService):
614
855
 
615
856
  @path_to_str()
616
857
  def dirlist(self, root, depth=-1):
858
+ """
859
+ List all files and directories recursively up to a specified depth.
860
+
861
+ :param root: Root directory to list
862
+ :param depth: Maximum depth to traverse (-1 for unlimited)
863
+ :yields: Full paths of files and directories
864
+ """
617
865
  for folder, dirs, files in self.walk(root):
618
866
  if folder == root:
619
867
  yield folder
@@ -625,55 +873,109 @@ class AfcService(LockdownService):
625
873
  yield posixpath.join(folder, entry)
626
874
 
627
875
  def lock(self, handle, operation):
628
- return self._do_operation(afc_opcode_t.FILE_LOCK, afc_lock_t.build({"handle": handle, "op": operation}))
629
-
630
- def _dispatch_packet(self, operation, data, this_length=0):
631
- afcpack = Container(
632
- magic=AFCMAGIC,
633
- entire_length=afc_header_t.sizeof() + len(data),
634
- this_length=afc_header_t.sizeof() + len(data),
635
- packet_num=self.packet_num,
636
- operation=operation,
876
+ """
877
+ Apply or remove an advisory lock on an open file.
878
+
879
+ :param handle: File handle returned from fopen
880
+ :param operation: Lock operation (AFC_LOCK_SH, AFC_LOCK_EX, or AFC_LOCK_UN)
881
+ :return: Response data from the operation
882
+ """
883
+ return self._do_operation(AfcOpcode.FILE_LOCK, afc_lock_t.build(AfcLockRequest(handle=handle, op=operation)))
884
+
885
+ def _dispatch_packet(self, operation: AfcOpcode, data: bytes, this_length: int = 0) -> None:
886
+ """
887
+ Send an AFC protocol packet to the device.
888
+
889
+ :param operation: AFC operation code
890
+ :param data: Packet payload data
891
+ :param this_length: Override for the packet length field (0 for auto-calculation)
892
+ """
893
+ entire_length = afc_header_t.sizeof() + len(data)
894
+ header = afc_header_t.build(
895
+ AfcHeader(
896
+ entire_length=entire_length,
897
+ this_length=this_length or entire_length,
898
+ packet_num=self.packet_num,
899
+ operation=operation,
900
+ )
637
901
  )
638
- if this_length:
639
- afcpack.this_length = this_length
640
- header = afc_header_t.build(afcpack)
641
902
  self.packet_num += 1
642
903
  self.service.sendall(header + data)
643
904
 
644
905
  def _receive_data(self):
645
- res = self.service.recvall(afc_header_t.sizeof())
646
- status = afc_error_t.SUCCESS
647
- data = ""
648
- if res:
649
- res = afc_header_t.parse(res)
650
- assert res["entire_length"] >= afc_header_t.sizeof()
651
- length = res["entire_length"] - afc_header_t.sizeof()
906
+ """
907
+ Receive an AFC protocol response packet from the device.
908
+
909
+ :return: Tuple of (status_code, response_data)
910
+ """
911
+ header_bytes = self.service.recvall(afc_header_t.sizeof())
912
+ status = AfcError.SUCCESS
913
+ data = b""
914
+ if header_bytes:
915
+ header = afc_header_t.parse(header_bytes)
916
+ assert header.entire_length >= afc_header_t.sizeof()
917
+ length = header.entire_length - afc_header_t.sizeof()
652
918
  data = self.service.recvall(length)
653
- if res.operation == afc_opcode_t.STATUS:
919
+ if header.operation == AfcOpcode.STATUS:
654
920
  if length != 8:
655
921
  self.logger.error("Status length != 8")
656
- status = afc_error_t.parse(data)
657
- elif res.operation != afc_opcode_t.DATA:
658
- pass
922
+ status = afc_error_construct.parse(data)
923
+ elif header.operation != AfcOpcode.DATA:
924
+ self.logger.debug("Unexpected AFC opcode %s", header.operation)
659
925
  return status, data
660
926
 
661
- def _do_operation(self, opcode, data: bytes = b""):
927
+ def _do_operation(self, opcode: AfcOpcode, data: bytes = b"", filename: Optional[str] = None) -> bytes:
928
+ """
929
+ Performs a low-level operation using the specified opcode and additional data.
930
+
931
+ This method dispatches a packet with the given opcode and data, waits for a
932
+ response, and processes the result to determine success or failure. If the
933
+ operation is unsuccessful, an appropriate exception is raised.
934
+
935
+ :param opcode: The operation code specifying the type of operation to perform.
936
+ :param data: The additional data to send along with the operation. Defaults to an empty byte string.
937
+ :param filename: The filename associated with the operation, if applicable. Defaults to None.
938
+
939
+ :returns: bytes: The data received as a response to the operation.
940
+
941
+ :raises:
942
+ AfcException: General exception raised if the operation fails with an
943
+ unspecified error status.
944
+ AfcFileNotFoundError: Exception raised when the operation fails due to
945
+ an object not being found (e.g., file or directory).
946
+ """
662
947
  self._dispatch_packet(opcode, data)
663
948
  status, data = self._receive_data()
664
949
 
665
950
  exception = AfcException
666
- if status != afc_error_t.SUCCESS:
667
- if status == afc_error_t.OBJECT_NOT_FOUND:
951
+ if status != AfcError.SUCCESS:
952
+ if status == AfcError.OBJECT_NOT_FOUND:
668
953
  exception = AfcFileNotFoundError
669
954
 
670
- raise exception(f"opcode: {opcode} failed with status: {status}", status)
955
+ opcode_name = opcode.name if isinstance(opcode, AfcOpcode) else opcode
956
+ message = f"Opcode: {opcode_name} failed with status: {status}"
957
+ if filename is not None:
958
+ message += f" for file: {filename}"
959
+ raise exception(message, status, filename)
671
960
 
672
961
  return data
673
962
 
674
963
 
675
964
  class AfcLsStub(LsStub):
965
+ """
966
+ Adapter class to make AfcShell compatible with pygnuutils ls implementation.
967
+
968
+ This stub provides an interface between the pygnuutils Ls class and the AFC
969
+ file system, translating calls to work with remote device paths.
970
+ """
971
+
676
972
  def __init__(self, afc_shell, stdout):
973
+ """
974
+ Initialize the ls stub.
975
+
976
+ :param afc_shell: AfcShell instance providing device access
977
+ :param stdout: Output stream for ls results
978
+ """
677
979
  self.afc_shell = afc_shell
678
980
  self.stdout = stdout
679
981
 
@@ -730,6 +1032,19 @@ class AfcLsStub(LsStub):
730
1032
 
731
1033
 
732
1034
  def path_completer(xsh, action, completer, alias, command) -> list[str]:
1035
+ """
1036
+ Provide path completion for xonsh shell commands.
1037
+
1038
+ Generates completion suggestions based on the current working directory
1039
+ and available files/directories on the device.
1040
+
1041
+ :param xsh: Xonsh shell instance
1042
+ :param action: Completion action
1043
+ :param completer: Completer instance
1044
+ :param alias: Command alias
1045
+ :param command: Command being completed
1046
+ :return: List of completion suggestions
1047
+ """
733
1048
  shell: AfcShell = XSH.ctx["_shell"]
734
1049
  pwd = shell.cwd
735
1050
  is_absolute = command.prefix.startswith("/")
@@ -753,6 +1068,18 @@ def path_completer(xsh, action, completer, alias, command) -> list[str]:
753
1068
 
754
1069
 
755
1070
  def dir_completer(xsh, action, completer, alias, command):
1071
+ """
1072
+ Provide directory-only completion for xonsh shell commands.
1073
+
1074
+ Similar to path_completer but only suggests directories, not files.
1075
+
1076
+ :param xsh: Xonsh shell instance
1077
+ :param action: Completion action
1078
+ :param completer: Completer instance
1079
+ :param alias: Command alias
1080
+ :param command: Command being completed
1081
+ :return: List of directory completion suggestions
1082
+ """
756
1083
  shell: AfcShell = XSH.ctx["_shell"]
757
1084
  pwd = shell.cwd
758
1085
  is_absolute = command.prefix.startswith("/")
@@ -774,6 +1101,19 @@ def dir_completer(xsh, action, completer, alias, command):
774
1101
 
775
1102
 
776
1103
  class AfcShell:
1104
+ """
1105
+ Interactive xonsh-based shell for navigating an iOS device's file system via AFC.
1106
+
1107
+ Provides a familiar shell environment with common commands (ls, cd, cat, etc.)
1108
+ that operate on the remote device's file system. The shell is powered by xonsh
1109
+ and includes features like tab completion, history, and command aliases.
1110
+
1111
+ Attributes:
1112
+ lockdown: Lockdown service provider for device communication
1113
+ afc: AfcService instance for file operations
1114
+ cwd: Current working directory on the device
1115
+ """
1116
+
777
1117
  @classmethod
778
1118
  def create(
779
1119
  cls,
@@ -782,6 +1122,17 @@ class AfcShell:
782
1122
  service: Optional[LockdownService] = None,
783
1123
  auto_cd: Optional[str] = "/",
784
1124
  ):
1125
+ """
1126
+ Create and launch an AFC shell session.
1127
+
1128
+ This class method sets up the xonsh environment and starts an interactive
1129
+ shell session for navigating the device's file system.
1130
+
1131
+ :param service_provider: Lockdown service provider for device connection
1132
+ :param service_name: Optional AFC service name override
1133
+ :param service: Optional pre-initialized AFC service instance
1134
+ :param auto_cd: Initial working directory (default: "/")
1135
+ """
785
1136
  warnings.filterwarnings("ignore", category=DeprecationWarning)
786
1137
  args = ["--rc", str(pathlib.Path(__file__).absolute())]
787
1138
  os.environ["XONSH_COLOR_STYLE"] = "default"
@@ -801,6 +1152,15 @@ class AfcShell:
801
1152
  pass
802
1153
 
803
1154
  def __init__(self, lockdown: LockdownServiceProvider, service: AfcService):
1155
+ """
1156
+ Initialize the AFC shell.
1157
+
1158
+ Sets up the shell environment, registers commands, and configures the prompt.
1159
+ This is called internally by the create() class method.
1160
+
1161
+ :param lockdown: Lockdown service provider for device communication
1162
+ :param service: AFC service instance for file operations
1163
+ """
804
1164
  self.lockdown = lockdown
805
1165
  self.afc = service
806
1166
  XSH.ctx["_shell"] = self
@@ -817,6 +1177,12 @@ class AfcShell:
817
1177
  """)
818
1178
 
819
1179
  def _register_arg_parse_alias(self, name: str, handler: Union[Callable, str]):
1180
+ """
1181
+ Register a command with argument parsing support.
1182
+
1183
+ :param name: Command name
1184
+ :param handler: Command handler function or string
1185
+ """
820
1186
  handler = ArgParserAlias(func=handler, has_args=True, prog=name)
821
1187
  self._commands[name] = handler
822
1188
  if XSH.aliases.get(name):
@@ -824,12 +1190,24 @@ class AfcShell:
824
1190
  XSH.aliases[name] = handler
825
1191
 
826
1192
  def _register_rpc_command(self, name, handler):
1193
+ """
1194
+ Register a simple command without argument parsing.
1195
+
1196
+ :param name: Command name
1197
+ :param handler: Command handler function or executable path
1198
+ """
827
1199
  self._commands[name] = handler
828
1200
  if XSH.aliases.get(name):
829
1201
  self._orig_aliases[name] = XSH.aliases[name]
830
1202
  XSH.aliases[name] = handler
831
1203
 
832
1204
  def _setup_shell_commands(self):
1205
+ """
1206
+ Initialize all shell commands and configure the shell environment.
1207
+
1208
+ Clears the PATH to prevent host command execution (except for specific
1209
+ utilities), registers AFC-specific commands, and sets up the custom prompt.
1210
+ """
833
1211
  # clear all host commands except for some useful ones
834
1212
  XSH.env["PATH"].clear()
835
1213
  # adding "file" just to fix xonsh errors
@@ -860,32 +1238,58 @@ class AfcShell:
860
1238
  XSH.env["PROMPT_FIELDS"]["prompt_end"] = self._prompt
861
1239
 
862
1240
  def _prompt(self) -> str:
1241
+ """
1242
+ Generate the prompt suffix based on the last command's exit status.
1243
+
1244
+ :return: Green '$' for success, red '$' for failure
1245
+ """
863
1246
  if len(XSH.history) == 0 or XSH.history[-1].rtn == 0:
864
1247
  return "{BOLD_GREEN}${RESET}"
865
1248
  return "{BOLD_RED}${RESET}"
866
1249
 
867
1250
  def _afc_cwd(self) -> str:
1251
+ """
1252
+ Get the current working directory for prompt display.
1253
+
1254
+ :return: Current working directory path
1255
+ """
868
1256
  return self.cwd
869
1257
 
870
1258
  def _relative_path(self, filename: str) -> str:
1259
+ """
1260
+ Convert a relative path to an absolute path based on cwd.
1261
+
1262
+ :param filename: Relative or absolute path
1263
+ :return: Absolute path
1264
+ """
871
1265
  return posixpath.join(self.cwd, filename)
872
1266
 
873
1267
  def _do_show_help(self):
874
- """
875
- list all rpc commands
876
- """
1268
+ """Display a list of all available shell commands."""
877
1269
  buf = ""
878
1270
  for k, _v in self._commands.items():
879
1271
  buf += f"👾 {k}\n"
880
1272
  print(buf)
881
1273
 
882
1274
  def _do_pwd(self) -> None:
1275
+ """Print the current working directory."""
883
1276
  print(self.cwd)
884
1277
 
885
1278
  def _do_link(self, target: str, source: str) -> None:
886
- self.afc.link(self.relative_path(target), self.relative_path(source), afc_link_type_t.SYMLINK)
1279
+ """
1280
+ Create a symbolic link on the device.
1281
+
1282
+ :param target: Target path that the link will point to
1283
+ :param source: Path where the link will be created
1284
+ """
1285
+ self.afc.link(self.relative_path(target), self.relative_path(source), AfcLinkType.SYMLINK)
887
1286
 
888
1287
  def _do_cd(self, directory: Annotated[str, Arg(completer=dir_completer)]) -> None:
1288
+ """
1289
+ Change the current working directory.
1290
+
1291
+ :param directory: Directory path to change to (relative or absolute)
1292
+ """
889
1293
  directory = self.relative_path(directory)
890
1294
  directory = posixpath.normpath(directory)
891
1295
  if self.afc.exists(directory):
@@ -895,7 +1299,11 @@ class AfcShell:
895
1299
  print(f"[ERROR] {directory} does not exist")
896
1300
 
897
1301
  def do_ls(self, args, stdin, stdout, stderr):
898
- """list files"""
1302
+ """
1303
+ List directory contents with Unix ls-like formatting.
1304
+
1305
+ Supports various ls options for formatting and filtering.
1306
+ """
899
1307
  try:
900
1308
  with ls_cli.make_context("ls", args) as ctx:
901
1309
  files = list(map(self._relative_path, ctx.params.pop("files")))
@@ -905,6 +1313,11 @@ class AfcShell:
905
1313
  pass
906
1314
 
907
1315
  def _do_walk(self, directory: Annotated[str, Arg(completer=dir_completer)]):
1316
+ """
1317
+ Recursively walk a directory tree and print all paths.
1318
+
1319
+ :param directory: Root directory to walk
1320
+ """
908
1321
  for root, dirs, files in self.afc.walk(self.relative_path(directory)):
909
1322
  for name in files:
910
1323
  print(posixpath.join(root, name))
@@ -912,9 +1325,19 @@ class AfcShell:
912
1325
  print(posixpath.join(root, name))
913
1326
 
914
1327
  def _do_cat(self, filename: str):
1328
+ """
1329
+ Display the contents of a file.
1330
+
1331
+ :param filename: Path to the file to display
1332
+ """
915
1333
  print(try_decode(self.afc.get_file_contents(self.relative_path(filename))))
916
1334
 
917
1335
  def _do_rm(self, file: Annotated[list[str], Arg(nargs="+", completer=path_completer)]):
1336
+ """
1337
+ Remove one or more files or directories.
1338
+
1339
+ :param file: List of file/directory paths to remove
1340
+ """
918
1341
  for filename in file:
919
1342
  self.afc.rm(self.relative_path(filename))
920
1343
 
@@ -922,18 +1345,22 @@ class AfcShell:
922
1345
  self,
923
1346
  remote_path: Annotated[str, Arg(completer=path_completer)],
924
1347
  local_path: str,
925
- ignore_errors: bool = False,
926
- progress_bar: bool = False,
1348
+ ignore_errors: Annotated[bool, Arg("--ignore-errors", action="store_true")] = False,
1349
+ progress_bar: Annotated[bool, Arg("--progress-bar", action="store_true")] = False,
927
1350
  ) -> None:
928
1351
  """
929
1352
  Pull a file or directory from device to local machine.
930
1353
 
931
1354
  Parameters
932
1355
  ----------
933
- ignore_errors : --ignore-errors
934
- Ignore errors and continue
935
- progress_bar : --progress_bar
936
- Show progress bar
1356
+ remote_path : str
1357
+ Path on the device to pull from
1358
+ local_path : str
1359
+ Local destination path
1360
+ ignore_errors : bool, optional
1361
+ Ignore errors and continue (--ignore-errors flag)
1362
+ progress_bar : bool, optional
1363
+ Show progress bar for large files (--progress_bar flag)
937
1364
  """
938
1365
 
939
1366
  def log(src, dst):
@@ -949,37 +1376,78 @@ class AfcShell:
949
1376
  )
950
1377
 
951
1378
  def _do_push(self, local_path: str, remote_path: Annotated[str, Arg(completer=path_completer)]):
1379
+ """
1380
+ Push a file or directory from local machine to device.
1381
+
1382
+ :param local_path: Local source path
1383
+ :param remote_path: Destination path on the device
1384
+ """
1385
+
952
1386
  def log(src, dst):
953
1387
  print(f"{src} --> {dst}")
954
1388
 
955
1389
  self.afc.push(local_path, self.relative_path(remote_path), callback=log)
956
1390
 
957
1391
  def _do_head(self, filename: Annotated[str, Arg(completer=path_completer)]):
1392
+ """
1393
+ Display the first 32 bytes of a file.
1394
+
1395
+ :param filename: Path to the file
1396
+ """
958
1397
  print(try_decode(self.afc.get_file_contents(self.relative_path(filename))[:32]))
959
1398
 
960
1399
  def _do_hexdump(self, filename: Annotated[str, Arg(completer=path_completer)]):
1400
+ """
1401
+ Display a hexadecimal dump of a file's contents.
1402
+
1403
+ :param filename: Path to the file
1404
+ """
961
1405
  print(hexdump.hexdump(self.afc.get_file_contents(self.relative_path(filename)), result="return"))
962
1406
 
963
1407
  def _do_mkdir(self, filename: Annotated[str, Arg(completer=path_completer)]):
1408
+ """
1409
+ Create a directory on the device.
1410
+
1411
+ :param filename: Path of the directory to create
1412
+ """
964
1413
  self.afc.makedirs(self.relative_path(filename))
965
1414
 
966
1415
  def _do_info(self):
1416
+ """Display device file system information."""
967
1417
  for k, v in self.afc.get_device_info().items():
968
1418
  print(f"{k}: {v}")
969
1419
 
970
1420
  def _do_mv(
971
1421
  self, source: Annotated[str, Arg(completer=path_completer)], dest: Annotated[str, Arg(completer=path_completer)]
972
1422
  ):
1423
+ """
1424
+ Move or rename a file or directory.
1425
+
1426
+ :param source: Source path
1427
+ :param dest: Destination path
1428
+ """
973
1429
  return self.afc.rename(self.relative_path(source), self.relative_path(dest))
974
1430
 
975
1431
  def _do_stat(self, filename: Annotated[str, Arg(completer=path_completer)]):
1432
+ """
1433
+ Display detailed file or directory statistics.
1434
+
1435
+ :param filename: Path to the file or directory
1436
+ """
976
1437
  for k, v in self.afc.stat(self.relative_path(filename)).items():
977
1438
  print(f"{k}: {v}")
978
1439
 
979
1440
  def relative_path(self, filename: str) -> str:
1441
+ """
1442
+ Convert a relative path to an absolute path based on cwd.
1443
+
1444
+ :param filename: Relative or absolute path
1445
+ :return: Absolute path
1446
+ """
980
1447
  return posixpath.join(self.cwd, filename)
981
1448
 
982
1449
  def _update_prompt(self) -> None:
1450
+ """Update the shell prompt with syntax highlighting."""
983
1451
  self.prompt = highlight(
984
1452
  f"[{self.afc.service_name}:{self.cwd}]$ ",
985
1453
  lexers.BashSessionLexer(),
@@ -987,6 +1455,15 @@ class AfcShell:
987
1455
  ).strip()
988
1456
 
989
1457
  def _complete(self, text, line, begidx, endidx):
1458
+ """
1459
+ Provide path completion for commands (internal method).
1460
+
1461
+ :param text: Current text being completed
1462
+ :param line: Full command line
1463
+ :param begidx: Beginning index of text
1464
+ :param endidx: Ending index of text
1465
+ :return: List of completion options
1466
+ """
990
1467
  curdir_diff = posixpath.dirname(text)
991
1468
  dirname = posixpath.join(self.cwd, curdir_diff)
992
1469
  prefix = posixpath.basename(text)
@@ -997,11 +1474,21 @@ class AfcShell:
997
1474
  ]
998
1475
 
999
1476
  def _complete_first_arg(self, text, line, begidx, endidx):
1477
+ """
1478
+ Complete only the first argument of a command.
1479
+
1480
+ :return: Completion options for first arg, empty list otherwise
1481
+ """
1000
1482
  if self._count_completion_parts(line, begidx) > 1:
1001
1483
  return []
1002
1484
  return self._complete(text, line, begidx, endidx)
1003
1485
 
1004
1486
  def _complete_push_arg(self, text, line, begidx, endidx):
1487
+ """
1488
+ Completion for push command (local path, then remote path).
1489
+
1490
+ :return: Local completions for first arg, remote for second
1491
+ """
1005
1492
  count = self._count_completion_parts(line, begidx)
1006
1493
  if count == 1:
1007
1494
  return self._complete_local(text)
@@ -1011,6 +1498,11 @@ class AfcShell:
1011
1498
  return []
1012
1499
 
1013
1500
  def _complete_pull_arg(self, text, line, begidx, endidx):
1501
+ """
1502
+ Completion for pull command (remote path, then local path).
1503
+
1504
+ :return: Remote completions for first arg, local for second
1505
+ """
1014
1506
  count = self._count_completion_parts(line, begidx)
1015
1507
  if count == 1:
1016
1508
  return self._complete(text, line, begidx, endidx)
@@ -1021,17 +1513,36 @@ class AfcShell:
1021
1513
 
1022
1514
  @staticmethod
1023
1515
  def _complete_local(text: str):
1516
+ """
1517
+ Provide local file system path completions.
1518
+
1519
+ :param text: Current text being completed
1520
+ :return: List of local path completions
1521
+ """
1024
1522
  path = pathlib.Path(text)
1025
1523
  path_iter = path.iterdir() if text.endswith(os.path.sep) else path.parent.iterdir()
1026
1524
  return [str(p) for p in path_iter if str(p).startswith(text)]
1027
1525
 
1028
1526
  @staticmethod
1029
1527
  def _count_completion_parts(line, begidx):
1528
+ """
1529
+ Count the number of space-separated parts in a command line.
1530
+
1531
+ :param line: Command line text
1532
+ :param begidx: Index to count parts up to
1533
+ :return: Number of parts
1534
+ """
1030
1535
  # Strip the " for paths including spaces.
1031
1536
  return len(shlex.split(line[:begidx].rstrip('"')))
1032
1537
 
1033
1538
 
1034
1539
  if __name__ == str(pathlib.Path(__file__).absolute()):
1540
+ """
1541
+ Entry point for xonsh RC script.
1542
+
1543
+ This block is executed when the file is loaded as an xonsh RC script,
1544
+ initializing the AFC shell with the context provided by AfcShell.create().
1545
+ """
1035
1546
  rc = XSH.ctx["_class"](XSH.ctx["_lockdown"], XSH.ctx["_service"])
1036
1547
  # fix fzf conflicts
1037
1548
  XSH.env["fzf_history_binding"] = "" # Ctrl+R