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