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