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 plist from '@expo/plist';
9import Debug from 'debug';
10import { Socket } from 'net';
11
12import { CommandError } from '../../../../utils/errors';
13import { parsePlistBuffer } from '../../../../utils/plist';
14
15const BPLIST_MAGIC = Buffer.from('bplist00');
16const debug = Debug('expo:apple-device:protocol');
17
18export class ProtocolClientError extends CommandError {
19  constructor(
20    msg: string,
21    public error: Error,
22    public protocolMessage?: any
23  ) {
24    super(msg);
25  }
26}
27
28export type ProtocolReaderCallback = (resp: any, err?: Error) => void;
29
30export class ProtocolReaderFactory<T> {
31  constructor(private ProtocolReader: new (callback: ProtocolReaderCallback) => T) {}
32
33  create(callback: (resp: any, err?: Error) => void): T {
34    return new this.ProtocolReader(callback);
35  }
36}
37
38export abstract class ProtocolReader {
39  protected body!: Buffer; // TODO: ! -> ?
40  protected bodyLength!: number; // TODO: ! -> ?
41  protected buffer = Buffer.alloc(0);
42  constructor(
43    protected headerSize: number,
44    protected callback: ProtocolReaderCallback
45  ) {
46    this.onData = this.onData.bind(this);
47  }
48
49  /** Returns length of body, or -1 if header doesn't contain length */
50  protected abstract parseHeader(data: Buffer): number;
51  protected abstract parseBody(data: Buffer): any;
52
53  onData(data?: Buffer) {
54    try {
55      // if there's data, add it on to existing buffer
56      this.buffer = data ? Buffer.concat([this.buffer, data]) : this.buffer;
57      // we haven't gotten the body length from the header yet
58      if (!this.bodyLength) {
59        if (this.buffer.length < this.headerSize) {
60          // partial header, wait for rest
61          return;
62        }
63        this.bodyLength = this.parseHeader(this.buffer);
64        // move on to body
65        this.buffer = this.buffer.slice(this.headerSize);
66        if (!this.buffer.length) {
67          // only got header, wait for body
68          return;
69        }
70      }
71      if (this.buffer.length < this.bodyLength) {
72        // wait for rest of body
73        return;
74      }
75
76      if (this.bodyLength === -1) {
77        this.callback(this.parseBody(this.buffer));
78        this.buffer = Buffer.alloc(0);
79      } else {
80        this.body = this.buffer.slice(0, this.bodyLength);
81        this.bodyLength -= this.body.length;
82        if (!this.bodyLength) {
83          this.callback(this.parseBody(this.body));
84        }
85        this.buffer = this.buffer.slice(this.body.length);
86        // There are multiple messages here, call parse again
87        if (this.buffer.length) {
88          this.onData();
89        }
90      }
91    } catch (err: any) {
92      this.callback(null, err);
93    }
94  }
95}
96
97export abstract class PlistProtocolReader extends ProtocolReader {
98  protected parseBody(body: Buffer) {
99    if (BPLIST_MAGIC.compare(body, 0, 8) === 0) {
100      return parsePlistBuffer(body);
101    } else {
102      return plist.parse(body.toString('utf8'));
103    }
104  }
105}
106
107export interface ProtocolWriter {
108  write(sock: Socket, msg: any): void;
109}
110
111export abstract class ProtocolClient<MessageType = any> {
112  constructor(
113    public socket: Socket,
114    protected readerFactory: ProtocolReaderFactory<ProtocolReader>,
115    protected writer: ProtocolWriter
116  ) {}
117
118  sendMessage<ResponseType = any>(msg: MessageType): Promise<ResponseType>;
119  sendMessage<CallbackType = void, ResponseType = any>(
120    msg: MessageType,
121    callback: (response: ResponseType, resolve: any, reject: any) => void
122  ): Promise<CallbackType>;
123  sendMessage<CallbackType = void, ResponseType = any>(
124    msg: MessageType,
125    callback?: (response: ResponseType, resolve: any, reject: any) => void
126  ): Promise<CallbackType | ResponseType> {
127    const onError = (error: Error) => {
128      debug('Unexpected protocol socket error encountered: %s', error);
129      throw new ProtocolClientError(
130        `Unexpected protocol error encountered: ${error.message}`,
131        error,
132        msg
133      );
134    };
135
136    return new Promise<ResponseType | CallbackType>((resolve, reject) => {
137      const reader = this.readerFactory.create(async (response: ResponseType, error?: Error) => {
138        if (error) {
139          reject(error);
140          return;
141        }
142        if (callback) {
143          callback(
144            response,
145            (value: any) => {
146              this.socket.removeListener('data', reader.onData);
147              this.socket.removeListener('error', onError);
148              resolve(value);
149            },
150            reject
151          );
152        } else {
153          this.socket.removeListener('data', reader.onData);
154          this.socket.removeListener('error', onError);
155          resolve(response);
156        }
157      });
158      this.socket.on('data', reader.onData);
159      this.socket.on('error', onError);
160      this.writer.write(this.socket, msg);
161    });
162  }
163}
164