• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2022 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {_TextDecoder} from 'custom_utils';
16
17import {defer, Deferred} from '../../base/deferred';
18import {assertFalse} from '../../base/logging';
19import {ArrayBufferBuilder} from '../../base/array_buffer_builder';
20
21import {RecordingError} from './recording_error_handling';
22import {ByteStream} from './recording_interfaces_v2';
23import {
24  BINARY_PUSH_FAILURE,
25  BINARY_PUSH_UNKNOWN_RESPONSE,
26} from './recording_utils';
27
28// https://cs.android.com/android/platform/superproject/+/main:packages/
29// modules/adb/file_sync_protocol.h;l=144
30const MAX_SYNC_SEND_CHUNK_SIZE = 64 * 1024;
31
32// Adb does not accurately send some file permissions. If you need a special set
33// of permissions, do not rely on this value. Rather, send a shell command which
34// explicitly sets permissions, such as:
35// 'shell:chmod ${permissions} ${path}'
36const FILE_PERMISSIONS = 2 ** 15 + 0o644;
37
38const textDecoder = new _TextDecoder();
39
40// For details about the protocol, see:
41// https://cs.android.com/android/platform/superproject/+/main:packages/modules/adb/SYNC.TXT
42export class AdbFileHandler {
43  private sentByteCount = 0;
44  private isPushOngoing: boolean = false;
45
46  constructor(private byteStream: ByteStream) {}
47
48  async pushBinary(binary: Uint8Array, path: string): Promise<void> {
49    // For a given byteStream, we only support pushing one binary at a time.
50    assertFalse(this.isPushOngoing);
51    this.isPushOngoing = true;
52    const transferFinished = defer<void>();
53
54    this.byteStream.addOnStreamDataCallback((data) =>
55      this.onStreamData(data, transferFinished),
56    );
57    this.byteStream.addOnStreamCloseCallback(
58      () => (this.isPushOngoing = false),
59    );
60
61    const sendMessage = new ArrayBufferBuilder();
62    // 'SEND' is the API method used to send a file to device.
63    sendMessage.append('SEND');
64    // The remote file name is split into two parts separated by the last
65    // comma (","). The first part is the actual path, while the second is a
66    // decimal encoded file mode containing the permissions of the file on
67    // device.
68    sendMessage.append(path.length + 6);
69    sendMessage.append(path);
70    sendMessage.append(',');
71    sendMessage.append(FILE_PERMISSIONS.toString());
72    this.byteStream.write(new Uint8Array(sendMessage.toArrayBuffer()));
73
74    while (!(await this.sendNextDataChunk(binary)));
75
76    return transferFinished;
77  }
78
79  private onStreamData(data: Uint8Array, transferFinished: Deferred<void>) {
80    this.sentByteCount = 0;
81    const response = textDecoder.decode(data);
82    if (response.split('\n')[0].includes('FAIL')) {
83      // Sample failure response (when the file is transferred successfully
84      // but the date is not formatted correctly):
85      // 'OKAYFAIL\npath too long'
86      transferFinished.reject(
87        new RecordingError(`${BINARY_PUSH_FAILURE}: ${response}`),
88      );
89    } else if (textDecoder.decode(data).substring(0, 4) === 'OKAY') {
90      // In case of success, the server responds to the last request with
91      // 'OKAY'.
92      transferFinished.resolve();
93    } else {
94      throw new RecordingError(`${BINARY_PUSH_UNKNOWN_RESPONSE}: ${response}`);
95    }
96  }
97
98  private async sendNextDataChunk(binary: Uint8Array): Promise<boolean> {
99    const endPosition = Math.min(
100      this.sentByteCount + MAX_SYNC_SEND_CHUNK_SIZE,
101      binary.byteLength,
102    );
103    const chunk = await binary.slice(this.sentByteCount, endPosition);
104    // The file is sent in chunks. Each chunk is prefixed with "DATA" and the
105    // chunk length. This is repeated until the entire file is transferred. Each
106    // chunk must not be larger than 64k.
107    const chunkLength = chunk.byteLength;
108    const dataMessage = new ArrayBufferBuilder();
109    dataMessage.append('DATA');
110    dataMessage.append(chunkLength);
111    dataMessage.append(
112      new Uint8Array(chunk.buffer, chunk.byteOffset, chunkLength),
113    );
114
115    this.sentByteCount += chunkLength;
116    const isDone = this.sentByteCount === binary.byteLength;
117
118    if (isDone) {
119      // When the file is transferred a sync request "DONE" is sent, together
120      // with a timestamp, representing the last modified time for the file. The
121      // server responds to this last request.
122      dataMessage.append('DONE');
123      // We send the date in seconds.
124      dataMessage.append(Math.floor(Date.now() / 1000));
125    }
126    this.byteStream.write(new Uint8Array(dataMessage.toArrayBuffer()));
127    return isDone;
128  }
129}
130