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.
- misc/understanding_idevice_protocol_layers.md +10 -5
- pymobiledevice3/__main__.py +171 -46
- pymobiledevice3/_version.py +2 -2
- pymobiledevice3/bonjour.py +22 -21
- pymobiledevice3/cli/activation.py +24 -22
- pymobiledevice3/cli/afc.py +49 -41
- pymobiledevice3/cli/amfi.py +13 -18
- pymobiledevice3/cli/apps.py +71 -65
- pymobiledevice3/cli/backup.py +134 -93
- pymobiledevice3/cli/bonjour.py +31 -29
- pymobiledevice3/cli/cli_common.py +175 -232
- pymobiledevice3/cli/companion_proxy.py +12 -12
- pymobiledevice3/cli/crash.py +95 -52
- pymobiledevice3/cli/developer/__init__.py +62 -0
- pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
- pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
- pymobiledevice3/cli/developer/arbitration.py +50 -0
- pymobiledevice3/cli/developer/condition.py +33 -0
- pymobiledevice3/cli/developer/core_device.py +294 -0
- pymobiledevice3/cli/developer/debugserver.py +244 -0
- pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
- pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
- pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
- pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
- pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
- pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
- pymobiledevice3/cli/developer/simulate_location.py +51 -0
- pymobiledevice3/cli/diagnostics/__init__.py +75 -0
- pymobiledevice3/cli/diagnostics/battery.py +47 -0
- pymobiledevice3/cli/idam.py +42 -0
- pymobiledevice3/cli/lockdown.py +70 -75
- pymobiledevice3/cli/mounter.py +99 -57
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +36 -20
- pymobiledevice3/cli/power_assertion.py +15 -16
- pymobiledevice3/cli/processes.py +11 -17
- pymobiledevice3/cli/profile.py +120 -75
- pymobiledevice3/cli/provision.py +27 -26
- pymobiledevice3/cli/remote.py +109 -100
- pymobiledevice3/cli/restore.py +134 -129
- pymobiledevice3/cli/springboard.py +50 -50
- pymobiledevice3/cli/syslog.py +145 -65
- pymobiledevice3/cli/usbmux.py +66 -27
- pymobiledevice3/cli/version.py +2 -5
- pymobiledevice3/cli/webinspector.py +232 -156
- pymobiledevice3/exceptions.py +6 -2
- pymobiledevice3/lockdown.py +5 -1
- pymobiledevice3/lockdown_service_provider.py +5 -0
- pymobiledevice3/remote/remote_service_discovery.py +18 -10
- pymobiledevice3/restore/device.py +28 -4
- pymobiledevice3/restore/restore.py +2 -2
- pymobiledevice3/service_connection.py +15 -12
- pymobiledevice3/services/afc.py +731 -220
- pymobiledevice3/services/device_link.py +45 -31
- pymobiledevice3/services/idam.py +20 -0
- pymobiledevice3/services/lockdown_service.py +12 -9
- pymobiledevice3/services/mobile_config.py +1 -0
- pymobiledevice3/services/mobilebackup2.py +6 -3
- pymobiledevice3/services/os_trace.py +97 -55
- pymobiledevice3/services/remote_fetch_symbols.py +13 -8
- pymobiledevice3/services/screenshot.py +2 -2
- pymobiledevice3/services/web_protocol/alert.py +8 -8
- pymobiledevice3/services/web_protocol/automation_session.py +87 -79
- pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
- pymobiledevice3/services/web_protocol/driver.py +71 -70
- pymobiledevice3/services/web_protocol/element.py +58 -56
- pymobiledevice3/services/web_protocol/selenium_api.py +47 -47
- pymobiledevice3/services/web_protocol/session_protocol.py +3 -2
- pymobiledevice3/services/web_protocol/switch_to.py +23 -19
- pymobiledevice3/services/webinspector.py +42 -67
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +5 -3
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/RECORD +76 -61
- pymobiledevice3/cli/completions.py +0 -50
- pymobiledevice3/cli/developer.py +0 -1539
- pymobiledevice3/cli/diagnostics.py +0 -110
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +0 -0
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/licenses/LICENSE +0 -0
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
pymobiledevice3/services/afc.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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":
|
|
142
|
-
"r+":
|
|
143
|
-
"w":
|
|
144
|
-
"w+":
|
|
145
|
-
"a":
|
|
146
|
-
"a+":
|
|
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 #
|
|
150
|
-
AFC_LOCK_EX = 2 | 4 #
|
|
151
|
-
AFC_LOCK_UN = 8 | 4 #
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 !=
|
|
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_=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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 !=
|
|
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(
|
|
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 !=
|
|
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(
|
|
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 !=
|
|
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",
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
|
919
|
+
if header.operation == AfcOpcode.STATUS:
|
|
654
920
|
if length != 8:
|
|
655
921
|
self.logger.error("Status length != 8")
|
|
656
|
-
status =
|
|
657
|
-
elif
|
|
658
|
-
|
|
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 !=
|
|
667
|
-
if status ==
|
|
951
|
+
if status != AfcError.SUCCESS:
|
|
952
|
+
if status == AfcError.OBJECT_NOT_FOUND:
|
|
668
953
|
exception = AfcFileNotFoundError
|
|
669
954
|
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|