1/* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import { 18 ArrayBufferBuilder, 19 BufferToken, 20 byteArrayToString, 21 ResizableBuffer, 22} from 'common/buffer_utils'; 23import {AdbWebSocketStream} from './adb_websocket_stream'; 24import {ErrorListener} from './websocket_stream'; 25 26export class SyncStream extends AdbWebSocketStream { 27 private static readonly DATA_ID = 'DATA'; 28 private static readonly DONE_ID = 'DONE'; 29 30 private cmdOut = new ResizableBuffer(); 31 private lastChunkOffset = 0; 32 33 constructor( 34 sock: WebSocket, 35 deviceSerialNumber: string, 36 errorListener: ErrorListener, 37 ) { 38 super(sock, deviceSerialNumber, 'sync', errorListener); 39 } 40 41 async pullFile(filepath: string): Promise<Uint8Array> { 42 return await new Promise<Uint8Array>((resolve) => { 43 this.onClose = () => { 44 resolve(this.cmdOut.get()); 45 }; 46 this.write(this.makeTokens(['RECV', filepath.length, filepath])); 47 }); 48 } 49 50 private makeTokens(tokens: BufferToken[]): Uint8Array { 51 const buffer = new ArrayBufferBuilder().append(tokens).build(); 52 return new Uint8Array(buffer); 53 } 54 55 protected override onData = (data: Uint8Array) => { 56 // add data from last chunk 57 const offset = Math.min(data.length, this.lastChunkOffset); 58 this.cmdOut.append(data.slice(0, offset)); 59 data = data.slice(offset); 60 this.lastChunkOffset = Math.max(0, this.lastChunkOffset - offset); 61 if (data.length === 0) { 62 return; 63 } 64 if (data.length < 8) { 65 console.error('Remaining data too small', data); 66 this.close(); 67 return; 68 } 69 70 // check start id of next chunk 71 const startId = byteArrayToString(data.slice(0, 4)); 72 const chunkLength = this.getChunkLength(data.slice(4, 8)); 73 if (data.length === 8 && startId === SyncStream.DONE_ID) { 74 this.close(); 75 return; 76 } 77 if (startId !== SyncStream.DATA_ID) { 78 console.error("expected 'DATA' id, received", startId); 79 this.close(); 80 return; 81 } 82 if (data.length === 8) { 83 this.lastChunkOffset = chunkLength; 84 return; 85 } 86 data = data.slice(8); 87 88 // check end id of remaining data 89 const endId = byteArrayToString( 90 data.slice(data.length - 8, data.length - 4), 91 ); 92 if (this.containsMultipleChunks(endId, chunkLength, data.length)) { 93 this.lastChunkOffset = 0; 94 this.cmdOut.append(data.slice(0, chunkLength)); 95 this.onData(data.slice(chunkLength)); 96 return; 97 } 98 99 // add remaining data 100 this.lastChunkOffset = chunkLength - data.length; 101 if (endId === SyncStream.DONE_ID) { 102 data = data.slice(0, data.length - 8); 103 } 104 this.cmdOut.append(data); 105 if (endId === SyncStream.DONE_ID) { 106 this.close(); 107 } 108 }; 109 110 private getChunkLength(data: Uint8Array) { 111 const dataView = new DataView( 112 data.buffer, 113 data.byteOffset, 114 data.byteLength, 115 ); 116 return dataView.getUint32(0, true); 117 } 118 119 private containsMultipleChunks( 120 endId: string, 121 chunkLength: number, 122 dataLength: number, 123 ) { 124 return ( 125 (endId !== SyncStream.DONE_ID && chunkLength < dataLength) || 126 (endId === SyncStream.DONE_ID && chunkLength < dataLength - 8) 127 ); 128 } 129} 130