1/** 2 * Copyright (c) 2021 Expo, Inc. 3 * Copyright (c) 2018 Drifty Co. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8import Debug from 'debug'; 9import { Socket } from 'net'; 10 11import { CommandError } from '../../../../utils/errors'; 12import type { ProtocolReaderCallback, ProtocolWriter } from './AbstractProtocol'; 13import { ProtocolClient, ProtocolReader, ProtocolReaderFactory } from './AbstractProtocol'; 14 15const debug = Debug('expo:apple-device:protocol:afc'); 16 17export const AFC_MAGIC = 'CFA6LPAA'; 18export const AFC_HEADER_SIZE = 40; 19 20export interface AFCHeader { 21 magic: typeof AFC_MAGIC; 22 totalLength: number; 23 headerLength: number; 24 requestId: number; 25 operation: AFC_OPS; 26} 27 28export interface AFCMessage { 29 operation: AFC_OPS; 30 data?: any; 31 payload?: any; 32} 33 34export interface AFCResponse { 35 operation: AFC_OPS; 36 id: number; 37 data: Buffer; 38} 39 40export interface AFCStatusResponse { 41 operation: AFC_OPS.STATUS; 42 id: number; 43 data: number; 44} 45 46/** 47 * AFC Operations 48 */ 49export enum AFC_OPS { 50 /** 51 * Invalid 52 */ 53 INVALID = 0x00000000, 54 55 /** 56 * Status 57 */ 58 STATUS = 0x00000001, 59 60 /** 61 * Data 62 */ 63 DATA = 0x00000002, 64 65 /** 66 * ReadDir 67 */ 68 READ_DIR = 0x00000003, 69 70 /** 71 * ReadFile 72 */ 73 READ_FILE = 0x00000004, 74 75 /** 76 * WriteFile 77 */ 78 WRITE_FILE = 0x00000005, 79 80 /** 81 * WritePart 82 */ 83 WRITE_PART = 0x00000006, 84 85 /** 86 * TruncateFile 87 */ 88 TRUNCATE = 0x00000007, 89 90 /** 91 * RemovePath 92 */ 93 REMOVE_PATH = 0x00000008, 94 95 /** 96 * MakeDir 97 */ 98 MAKE_DIR = 0x00000009, 99 100 /** 101 * GetFileInfo 102 */ 103 GET_FILE_INFO = 0x0000000a, 104 105 /** 106 * GetDeviceInfo 107 */ 108 GET_DEVINFO = 0x0000000b, 109 110 /** 111 * WriteFileAtomic (tmp file+rename) 112 */ 113 WRITE_FILE_ATOM = 0x0000000c, 114 115 /** 116 * FileRefOpen 117 */ 118 FILE_OPEN = 0x0000000d, 119 120 /** 121 * FileRefOpenResult 122 */ 123 FILE_OPEN_RES = 0x0000000e, 124 125 /** 126 * FileRefRead 127 */ 128 FILE_READ = 0x0000000f, 129 130 /** 131 * FileRefWrite 132 */ 133 FILE_WRITE = 0x00000010, 134 135 /** 136 * FileRefSeek 137 */ 138 FILE_SEEK = 0x00000011, 139 140 /** 141 * FileRefTell 142 */ 143 FILE_TELL = 0x00000012, 144 145 /** 146 * FileRefTellResult 147 */ 148 FILE_TELL_RES = 0x00000013, 149 150 /** 151 * FileRefClose 152 */ 153 FILE_CLOSE = 0x00000014, 154 155 /** 156 * FileRefSetFileSize (ftruncate) 157 */ 158 FILE_SET_SIZE = 0x00000015, 159 160 /** 161 * GetConnectionInfo 162 */ 163 GET_CON_INFO = 0x00000016, 164 165 /** 166 * SetConnectionOptions 167 */ 168 SET_CON_OPTIONS = 0x00000017, 169 170 /** 171 * RenamePath 172 */ 173 RENAME_PATH = 0x00000018, 174 175 /** 176 * SetFSBlockSize (0x800000) 177 */ 178 SET_FS_BS = 0x00000019, 179 180 /** 181 * SetSocketBlockSize (0x800000) 182 */ 183 SET_SOCKET_BS = 0x0000001a, 184 185 /** 186 * FileRefLock 187 */ 188 FILE_LOCK = 0x0000001b, 189 190 /** 191 * MakeLink 192 */ 193 MAKE_LINK = 0x0000001c, 194 195 /** 196 * GetFileHash 197 */ 198 GET_FILE_HASH = 0x0000001d, 199 200 /** 201 * SetModTime 202 */ 203 SET_FILE_MOD_TIME = 0x0000001e, 204 205 /** 206 * GetFileHashWithRange 207 */ 208 GET_FILE_HASH_RANGE = 0x0000001f, 209 210 // iOS 6+ 211 212 /** 213 * FileRefSetImmutableHint 214 */ 215 FILE_SET_IMMUTABLE_HINT = 0x00000020, 216 217 /** 218 * GetSizeOfPathContents 219 */ 220 GET_SIZE_OF_PATH_CONTENTS = 0x00000021, 221 222 /** 223 * RemovePathAndContents 224 */ 225 REMOVE_PATH_AND_CONTENTS = 0x00000022, 226 227 /** 228 * DirectoryEnumeratorRefOpen 229 */ 230 DIR_OPEN = 0x00000023, 231 232 /** 233 * DirectoryEnumeratorRefOpenResult 234 */ 235 DIR_OPEN_RESULT = 0x00000024, 236 237 /** 238 * DirectoryEnumeratorRefRead 239 */ 240 DIR_READ = 0x00000025, 241 242 /** 243 * DirectoryEnumeratorRefClose 244 */ 245 DIR_CLOSE = 0x00000026, 246 247 // iOS 7+ 248 249 /** 250 * FileRefReadWithOffset 251 */ 252 FILE_READ_OFFSET = 0x00000027, 253 254 /** 255 * FileRefWriteWithOffset 256 */ 257 FILE_WRITE_OFFSET = 0x00000028, 258} 259 260/** 261 * Error Codes 262 */ 263export enum AFC_STATUS { 264 SUCCESS = 0, 265 UNKNOWN_ERROR = 1, 266 OP_HEADER_INVALID = 2, 267 NO_RESOURCES = 3, 268 READ_ERROR = 4, 269 WRITE_ERROR = 5, 270 UNKNOWN_PACKET_TYPE = 6, 271 INVALID_ARG = 7, 272 OBJECT_NOT_FOUND = 8, 273 OBJECT_IS_DIR = 9, 274 PERM_DENIED = 10, 275 SERVICE_NOT_CONNECTED = 11, 276 OP_TIMEOUT = 12, 277 TOO_MUCH_DATA = 13, 278 END_OF_DATA = 14, 279 OP_NOT_SUPPORTED = 15, 280 OBJECT_EXISTS = 16, 281 OBJECT_BUSY = 17, 282 NO_SPACE_LEFT = 18, 283 OP_WOULD_BLOCK = 19, 284 IO_ERROR = 20, 285 OP_INTERRUPTED = 21, 286 OP_IN_PROGRESS = 22, 287 INTERNAL_ERROR = 23, 288 MUX_ERROR = 30, 289 NO_MEM = 31, 290 NOT_ENOUGH_DATA = 32, 291 DIR_NOT_EMPTY = 33, 292 FORCE_SIGNED_TYPE = -1, 293} 294 295export enum AFC_FILE_OPEN_FLAGS { 296 /** 297 * r (O_RDONLY) 298 */ 299 RDONLY = 0x00000001, 300 301 /** 302 * r+ (O_RDWR | O_CREAT) 303 */ 304 RW = 0x00000002, 305 306 /** 307 * w (O_WRONLY | O_CREAT | O_TRUNC) 308 */ 309 WRONLY = 0x00000003, 310 311 /** 312 * w+ (O_RDWR | O_CREAT | O_TRUNC) 313 */ 314 WR = 0x00000004, 315 316 /** 317 * a (O_WRONLY | O_APPEND | O_CREAT) 318 */ 319 APPEND = 0x00000005, 320 321 /** 322 * a+ (O_RDWR | O_APPEND | O_CREAT) 323 */ 324 RDAPPEND = 0x00000006, 325} 326 327function isAFCResponse(resp: any): resp is AFCResponse { 328 return AFC_OPS[resp.operation] !== undefined && resp.id !== undefined && resp.data !== undefined; 329} 330 331function isStatusResponse(resp: any): resp is AFCStatusResponse { 332 return isAFCResponse(resp) && resp.operation === AFC_OPS.STATUS; 333} 334 335function isErrorStatusResponse(resp: AFCResponse): boolean { 336 return isStatusResponse(resp) && resp.data !== AFC_STATUS.SUCCESS; 337} 338 339class AFCInternalError extends Error { 340 constructor(msg: string, public requestId: number) { 341 super(msg); 342 } 343} 344 345export class AFCError extends Error { 346 constructor(msg: string, public status: AFC_STATUS) { 347 super(msg); 348 } 349} 350 351export class AFCProtocolClient extends ProtocolClient { 352 private requestId = 0; 353 private requestCallbacks: { [key: number]: ProtocolReaderCallback } = {}; 354 355 constructor(socket: Socket) { 356 super(socket, new ProtocolReaderFactory(AFCProtocolReader), new AFCProtocolWriter()); 357 358 const reader = this.readerFactory.create((resp, err) => { 359 if (err && err instanceof AFCInternalError) { 360 this.requestCallbacks[err.requestId](resp, err); 361 } else if (isErrorStatusResponse(resp)) { 362 this.requestCallbacks[resp.id](resp, new AFCError(AFC_STATUS[resp.data], resp.data)); 363 } else { 364 this.requestCallbacks[resp.id](resp); 365 } 366 }); 367 socket.on('data', reader.onData); 368 } 369 370 sendMessage(msg: AFCMessage): Promise<AFCResponse> { 371 return new Promise<AFCResponse>((resolve, reject) => { 372 const requestId = this.requestId++; 373 this.requestCallbacks[requestId] = async (resp: any, err?: Error) => { 374 if (err) { 375 reject(err); 376 return; 377 } 378 if (isAFCResponse(resp)) { 379 resolve(resp); 380 } else { 381 reject(new CommandError('APPLE_DEVICE_AFC', 'Malformed AFC response')); 382 } 383 }; 384 this.writer.write(this.socket, { ...msg, requestId }); 385 }); 386 } 387} 388 389export class AFCProtocolReader extends ProtocolReader { 390 private header!: AFCHeader; // TODO: ! -> ? 391 392 constructor(callback: ProtocolReaderCallback) { 393 super(AFC_HEADER_SIZE, callback); 394 } 395 396 parseHeader(data: Buffer) { 397 const magic = data.slice(0, 8).toString('ascii'); 398 if (magic !== AFC_MAGIC) { 399 throw new AFCInternalError( 400 `Invalid AFC packet received (magic != ${AFC_MAGIC})`, 401 data.readUInt32LE(24) 402 ); 403 } 404 // technically these are uint64 405 this.header = { 406 magic, 407 totalLength: data.readUInt32LE(8), 408 headerLength: data.readUInt32LE(16), 409 requestId: data.readUInt32LE(24), 410 operation: data.readUInt32LE(32), 411 }; 412 413 debug(`parse header: ${JSON.stringify(this.header)}`); 414 if (this.header.headerLength < AFC_HEADER_SIZE) { 415 throw new AFCInternalError('Invalid AFC header', this.header.requestId); 416 } 417 return this.header.totalLength - AFC_HEADER_SIZE; 418 } 419 420 parseBody(data: Buffer): AFCResponse | AFCStatusResponse { 421 const body: any = { 422 operation: this.header.operation, 423 id: this.header.requestId, 424 data, 425 }; 426 if (isStatusResponse(body)) { 427 const status = data.readUInt32LE(0); 428 debug(`${AFC_OPS[this.header.operation]} response: ${AFC_STATUS[status]}`); 429 body.data = status; 430 } else if (data.length <= 8) { 431 debug(`${AFC_OPS[this.header.operation]} response: ${Array.prototype.toString.call(body)}`); 432 } else { 433 debug(`${AFC_OPS[this.header.operation]} response length: ${data.length} bytes`); 434 } 435 return body; 436 } 437} 438 439export class AFCProtocolWriter implements ProtocolWriter { 440 write(socket: Socket, msg: AFCMessage & { requestId: number }) { 441 const { data, payload, operation, requestId } = msg; 442 443 const dataLength = data ? data.length : 0; 444 const payloadLength = payload ? payload.length : 0; 445 446 const header = Buffer.alloc(AFC_HEADER_SIZE); 447 const magic = Buffer.from(AFC_MAGIC); 448 magic.copy(header); 449 header.writeUInt32LE(AFC_HEADER_SIZE + dataLength + payloadLength, 8); 450 header.writeUInt32LE(AFC_HEADER_SIZE + dataLength, 16); 451 header.writeUInt32LE(requestId, 24); 452 header.writeUInt32LE(operation, 32); 453 socket.write(header); 454 socket.write(data); 455 if (data.length <= 8) { 456 debug( 457 `socket write, header: { requestId: ${requestId}, operation: ${ 458 AFC_OPS[operation] 459 }}, body: ${Array.prototype.toString.call(data)}` 460 ); 461 } else { 462 debug( 463 `socket write, header: { requestId: ${requestId}, operation: ${AFC_OPS[operation]}}, body: ${data.length} bytes` 464 ); 465 } 466 467 debug(`socket write, bytes written ${header.length} (header), ${data.length} (body)`); 468 if (payload) { 469 socket.write(payload); 470 } 471 } 472} 473