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 * as fs from 'fs';
10import { Socket } from 'net';
11
12import type { LockdownCommand, LockdownResponse } from '../protocol/LockdownProtocol';
13import { isLockdownResponse, LockdownProtocolClient } from '../protocol/LockdownProtocol';
14import { ResponseError, ServiceClient } from './ServiceClient';
15
16const debug = Debug('expo:apple-device:client:mobile_image_mounter');
17
18export type MIMMountResponse = LockdownResponse;
19
20export interface MIMMessage extends LockdownCommand {
21  ImageType: string;
22}
23
24export interface MIMLookupResponse extends LockdownResponse {
25  ImageSignature?: string;
26}
27
28export interface MIMUploadCompleteResponse extends LockdownResponse {
29  Status: 'Complete';
30}
31
32export interface MIMUploadReceiveBytesResponse extends LockdownResponse {
33  Status: 'ReceiveBytesAck';
34}
35
36function isMIMUploadCompleteResponse(resp: any): resp is MIMUploadCompleteResponse {
37  return resp.Status === 'Complete';
38}
39
40function isMIMUploadReceiveBytesResponse(resp: any): resp is MIMUploadReceiveBytesResponse {
41  return resp.Status === 'ReceiveBytesAck';
42}
43
44export class MobileImageMounterClient extends ServiceClient<LockdownProtocolClient<MIMMessage>> {
45  constructor(socket: Socket) {
46    super(socket, new LockdownProtocolClient(socket));
47  }
48
49  async mountImage(imagePath: string, imageSig: Buffer) {
50    debug(`mountImage: ${imagePath}`);
51
52    const resp = await this.protocolClient.sendMessage({
53      Command: 'MountImage',
54      ImagePath: imagePath,
55      ImageSignature: imageSig,
56      ImageType: 'Developer',
57    });
58
59    if (!isLockdownResponse(resp) || resp.Status !== 'Complete') {
60      throw new ResponseError(`There was an error mounting ${imagePath} on device`, resp);
61    }
62  }
63
64  async uploadImage(imagePath: string, imageSig: Buffer) {
65    debug(`uploadImage: ${imagePath}`);
66
67    const imageSize = fs.statSync(imagePath).size;
68    return this.protocolClient.sendMessage(
69      {
70        Command: 'ReceiveBytes',
71        ImageSize: imageSize,
72        ImageSignature: imageSig,
73        ImageType: 'Developer',
74      },
75      (resp: any, resolve, reject) => {
76        if (isMIMUploadReceiveBytesResponse(resp)) {
77          const imageStream = fs.createReadStream(imagePath);
78          imageStream.pipe(this.protocolClient.socket, { end: false });
79          imageStream.on('error', (err) => reject(err));
80        } else if (isMIMUploadCompleteResponse(resp)) {
81          resolve();
82        } else {
83          reject(
84            new ResponseError(`There was an error uploading image ${imagePath} to the device`, resp)
85          );
86        }
87      }
88    );
89  }
90
91  async lookupImage() {
92    debug('lookupImage');
93
94    return this.protocolClient.sendMessage<MIMLookupResponse>({
95      Command: 'LookupImage',
96      ImageType: 'Developer',
97    });
98  }
99}
100