• 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 '../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/+/master: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/+/master: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(
55        (data) => this.onStreamData(data, transferFinished));
56    this.byteStream.addOnStreamCloseCallback(() => this.isPushOngoing = false);
57
58    const sendMessage = new ArrayBufferBuilder();
59    // 'SEND' is the API method used to send a file to device.
60    sendMessage.append('SEND');
61    // The remote file name is split into two parts separated by the last
62    // comma (","). The first part is the actual path, while the second is a
63    // decimal encoded file mode containing the permissions of the file on
64    // device.
65    sendMessage.append(path.length + 6);
66    sendMessage.append(path);
67    sendMessage.append(',');
68    sendMessage.append(FILE_PERMISSIONS.toString());
69    this.byteStream.write(new Uint8Array(sendMessage.toArrayBuffer()));
70
71    while (!(await this.sendNextDataChunk(binary)))
72      ;
73
74    return transferFinished;
75  }
76
77  private onStreamData(data: Uint8Array, transferFinished: Deferred<void>) {
78    this.sentByteCount = 0;
79    const response = textDecoder.decode(data);
80    if (response.split('\n')[0].includes('FAIL')) {
81      // Sample failure response (when the file is transferred successfully
82      // but the date is not formatted correctly):
83      // 'OKAYFAIL\npath too long'
84      transferFinished.reject(
85          new RecordingError(`${BINARY_PUSH_FAILURE}: ${response}`));
86    } else if (textDecoder.decode(data).substring(0, 4) === 'OKAY') {
87      // In case of success, the server responds to the last request with
88      // 'OKAY'.
89      transferFinished.resolve();
90    } else {
91      throw new RecordingError(`${BINARY_PUSH_UNKNOWN_RESPONSE}: ${response}`);
92    }
93  }
94
95  private async sendNextDataChunk(binary: Uint8Array): Promise<boolean> {
96    const endPosition = Math.min(
97        this.sentByteCount + MAX_SYNC_SEND_CHUNK_SIZE, binary.byteLength);
98    const chunk = await binary.slice(this.sentByteCount, endPosition);
99    // The file is sent in chunks. Each chunk is prefixed with "DATA" and the
100    // chunk length. This is repeated until the entire file is transferred. Each
101    // chunk must not be larger than 64k.
102    const chunkLength = chunk.byteLength;
103    const dataMessage = new ArrayBufferBuilder();
104    dataMessage.append('DATA');
105    dataMessage.append(chunkLength);
106    dataMessage.append(
107        new Uint8Array(chunk.buffer, chunk.byteOffset, chunkLength));
108
109    this.sentByteCount += chunkLength;
110    const isDone = this.sentByteCount === binary.byteLength;
111
112    if (isDone) {
113      // When the file is transferred a sync request "DONE" is sent, together
114      // with a timestamp, representing the last modified time for the file. The
115      // server responds to this last request.
116      dataMessage.append('DONE');
117      // We send the date in seconds.
118      dataMessage.append(Math.floor(Date.now() / 1000));
119    }
120    this.byteStream.write(new Uint8Array(dataMessage.toArrayBuffer()));
121    return isDone;
122  }
123}
124