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