refactor: split core and web packages

This commit is contained in:
Simon Chan 2020-08-27 00:01:25 +08:00
parent c1b437999c
commit 14a10a65d5
33 changed files with 925 additions and 502 deletions

View file

@ -4,7 +4,9 @@
"CNXN", "CNXN",
"RSASSA", "RSASSA",
"WRTE", "WRTE",
"addrs",
"fluentui", "fluentui",
"getprop",
"lapo", "lapo",
"reimplement", "reimplement",
"tcpip", "tcpip",

View file

@ -3,8 +3,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"postinstall": "lerna bootstrap", "postinstall": "lerna bootstrap",
"build": "lerna run --scope @yume-chan/webadb build", "build": "lerna run --scope @yume-chan/adb-webusb build",
"build:watch": "lerna run --scope @yume-chan/webadb --stream build:watch", "build:watch": "lerna run --scope @yume-chan/adb-webusb --stream build:watch",
"build:demo": "lerna run --scope demo --stream build", "build:demo": "lerna run --scope demo --stream build",
"start:demo": "lerna run --scope demo --stream start" "start:demo": "lerna run --scope demo --stream start"
}, },

View file

@ -0,0 +1,11 @@
# `adb-webusb`
> TODO: description
## Usage
```
const adbWebusb = require('adb-webusb');
// TODO: DEMONSTRATE API
```

24
packages/adb-webusb/package-lock.json generated Normal file
View file

@ -0,0 +1,24 @@
{
"name": "@yume-chan/adb-webusb",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/w3c-web-usb": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.4.tgz",
"integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A=="
},
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
},
"typescript": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz",
"integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==",
"dev": true
}
}
}

View file

@ -0,0 +1,33 @@
{
"name": "@yume-chan/adb-webusb",
"version": "0.0.1",
"description": "WebUSB backend for @yume-chan/adb",
"keywords": [
"webusb",
"adb"
],
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git"
},
"scripts": {
"build": "tsc -b",
"build:watch": "tsc -b -w"
},
"bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues"
},
"devDependencies": {
"typescript": "4.0.2"
},
"dependencies": {
"@types/w3c-web-usb": "1.0.4",
"@yume-chan/adb": "^0.0.1",
"@yume-chan/event": "^0.0.1",
"tslib": "2.0.1"
}
}

View file

@ -1,12 +1,5 @@
export interface WebAdbTransportation { import { AdbBackend, decodeBase64, encodeBase64 } from '@yume-chan/adb';
readonly name: string | undefined; import { EventEmitter } from '@yume-chan/event';
write(buffer: ArrayBuffer): void | Promise<void>;
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
dispose(): void | Promise<void>;
}
export const WebUsbDeviceFilter: USBDeviceFilter = { export const WebUsbDeviceFilter: USBDeviceFilter = {
classCode: 0xFF, classCode: 0xFF,
@ -14,7 +7,12 @@ export const WebUsbDeviceFilter: USBDeviceFilter = {
protocolCode: 1, protocolCode: 1,
}; };
export class WebUsbTransportation implements WebAdbTransportation { const PrivateKeyStorageKey = 'private-key';
const Utf8Encoder = new TextEncoder();
const Utf8Decoder = new TextDecoder();
export class WebUsbAdbBackend implements AdbBackend {
public static async fromDevice(device: USBDevice) { public static async fromDevice(device: USBDevice) {
await device.open(); await device.open();
@ -44,13 +42,13 @@ export class WebUsbTransportation implements WebAdbTransportation {
case 'in': case 'in':
inEndpointNumber = endpoint.endpointNumber; inEndpointNumber = endpoint.endpointNumber;
if (outEndpointNumber !== undefined) { if (outEndpointNumber !== undefined) {
return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber); return new WebUsbAdbBackend(device, inEndpointNumber, outEndpointNumber);
} }
break; break;
case 'out': case 'out':
outEndpointNumber = endpoint.endpointNumber; outEndpointNumber = endpoint.endpointNumber;
if (inEndpointNumber !== undefined) { if (inEndpointNumber !== undefined) {
return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber); return new WebUsbAdbBackend(device, inEndpointNumber, outEndpointNumber);
} }
break; break;
} }
@ -66,7 +64,7 @@ export class WebUsbTransportation implements WebAdbTransportation {
public static async pickDevice() { public static async pickDevice() {
try { try {
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] }); const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
return WebUsbTransportation.fromDevice(device); return WebUsbAdbBackend.fromDevice(device);
} catch (e) { } catch (e) {
switch (e.name) { switch (e.name) {
case 'NotFoundError': case 'NotFoundError':
@ -81,6 +79,9 @@ export class WebUsbTransportation implements WebAdbTransportation {
public get name() { return this._device.productName; } public get name() { return this._device.productName; }
private readonly onDisconnectedEvent = new EventEmitter<void>();
public readonly onDisconnected = this.onDisconnectedEvent.event;
private _inEndpointNumber!: number; private _inEndpointNumber!: number;
private _outEndpointNumber!: number; private _outEndpointNumber!: number;
@ -90,21 +91,63 @@ export class WebUsbTransportation implements WebAdbTransportation {
this._outEndpointNumber = outEndPointNumber; this._outEndpointNumber = outEndPointNumber;
} }
public *iterateKeys(): Generator<ArrayBuffer, void, void> {
const privateKey = window.localStorage.getItem(PrivateKeyStorageKey);
if (privateKey) {
yield decodeBase64(privateKey);
}
}
public async generateKey(): Promise<ArrayBuffer> {
const { privateKey: cryptoKey } = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
// 65537
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: 'SHA-1',
},
true,
['sign', 'verify']
);
const privateKey = await crypto.subtle.exportKey('pkcs8', cryptoKey);
window.localStorage.setItem(PrivateKeyStorageKey, encodeBase64(privateKey));
return privateKey;
}
public encodeUtf8(input: string): ArrayBuffer {
return Utf8Encoder.encode(input);
}
public decodeUtf8(buffer: ArrayBuffer): string {
return Utf8Decoder.decode(buffer);
}
public async write(buffer: ArrayBuffer): Promise<void> { public async write(buffer: ArrayBuffer): Promise<void> {
await this._device.transferOut(this._outEndpointNumber, buffer); await this._device.transferOut(this._outEndpointNumber, buffer);
} }
public async read(length: number): Promise<ArrayBuffer> { public async read(length: number): Promise<ArrayBuffer> {
const result = await this._device.transferIn(this._inEndpointNumber, length); try {
const result = await this._device.transferIn(this._inEndpointNumber, length);
if (result.status === 'stall') { if (result.status === 'stall') {
await this._device.clearHalt('in', this._inEndpointNumber); await this._device.clearHalt('in', this._inEndpointNumber);
}
return result.data!.buffer;
} catch (e) {
if (e instanceof Error && e.name === 'NotFoundError') {
this.onDisconnectedEvent.fire();
}
throw e;
} }
return result.data!.buffer;
} }
public async dispose() { public async dispose() {
this.onDisconnectedEvent.dispose();
await this._device.close(); await this._device.close();
} }
} }

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "lib", // /* Redirect output structure to the directory. */
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"lib": [
"ESNext",
"DOM"
]
},
"include": [
"src"
],
"references": [
{
"path": "../adb/tsconfig.json"
}
]
}

21
packages/adb/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Simon Chan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,14 +1,9 @@
{ {
"name": "@yume-chan/webadb", "name": "@yume-chan/adb",
"version": "0.0.1", "version": "0.0.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@types/w3c-web-usb": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.4.tgz",
"integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A=="
},
"@yume-chan/async-operation-manager": { "@yume-chan/async-operation-manager": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-2.0.0.tgz",

View file

@ -1,10 +1,9 @@
{ {
"name": "@yume-chan/webadb", "name": "@yume-chan/adb",
"version": "0.0.1", "version": "0.0.1",
"description": "ADB (Android Debugging Bridge) for Web browsers", "description": "ADB (Android Debugging Bridge) for Web browsers",
"keywords": [ "keywords": [
"adb", "adb"
"webusb"
], ],
"author": "Simon Chan <cnsimonchan@live.com>", "author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme", "homepage": "https://github.com/yume-chan/ya-webadb#readme",
@ -25,7 +24,6 @@
"typescript": "4.0.2" "typescript": "4.0.2"
}, },
"dependencies": { "dependencies": {
"@types/w3c-web-usb": "^1.0.4",
"@yume-chan/async-operation-manager": "^2.0.0", "@yume-chan/async-operation-manager": "^2.0.0",
"@yume-chan/event": "^0.0.1", "@yume-chan/event": "^0.0.1",
"tslib": "^2.0.1" "tslib": "^2.0.1"

View file

@ -1,23 +1,9 @@
import { PromiseResolver } from '@yume-chan/async-operation-manager'; import { PromiseResolver } from '@yume-chan/async-operation-manager';
import { AdbAuthHandler, PublicKeyAuthMethod, SignatureAuthMethod } from './auth'; import { AutoDisposable, DisposableList } from '@yume-chan/event';
import { AdbPacket } from './packet'; import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth';
import { AdbStream, AdbStreamDispatcher } from './stream'; import { AdbBackend } from './backend';
import { WebAdbTransportation } from './transportation'; import { AdbCommand } from './packet';
import { AdbPacketDispatcher, AdbStream } from './stream';
export enum AdbCommand {
Connect = 'CNXN',
Auth = 'AUTH',
OK = 'OKAY',
Close = 'CLSE',
Write = 'WRTE',
Open = 'OPEN',
}
export enum AdbAuthType {
Token = 1,
Signature = 2,
PublicKey = 3,
}
export enum AdbPropKey { export enum AdbPropKey {
Product = 'ro.product.name', Product = 'ro.product.name',
@ -26,10 +12,15 @@ export enum AdbPropKey {
Features = 'features', Features = 'features',
} }
export class WebAdb { export class Adb {
private _transportation: WebAdbTransportation; private backend: AdbBackend;
public get name() { return this._transportation.name; } public get onDisconnected() { return this.backend.onDisconnected; }
private _connected = false;
public get connected() { return this._connected; }
public get name() { return this.backend.name; }
private _product: string | undefined; private _product: string | undefined;
public get product() { return this._product; } public get product() { return this._product; }
@ -43,14 +34,16 @@ export class WebAdb {
private _features: string[] | undefined; private _features: string[] | undefined;
public get features() { return this._features; } public get features() { return this._features; }
private streamDispatcher: AdbStreamDispatcher; private packetDispatcher: AdbPacketDispatcher;
public constructor(transportation: WebAdbTransportation) { public constructor(backend: AdbBackend) {
this._transportation = transportation; this.backend = backend;
this.streamDispatcher = new AdbStreamDispatcher(transportation); this.packetDispatcher = new AdbPacketDispatcher(backend);
backend.onDisconnected(this.dispose, this);
} }
public async connect() { public async connect(authenticators = AdbDefaultAuthenticators) {
const version = 0x01000001; const version = 0x01000001;
const features = [ const features = [
@ -73,8 +66,9 @@ export class WebAdb {
].join(','); ].join(',');
const resolver = new PromiseResolver<void>(); const resolver = new PromiseResolver<void>();
const authHandler = new AdbAuthHandler([new SignatureAuthMethod(), PublicKeyAuthMethod]); const authHandler = new AdbAuthenticationHandler(authenticators, this.backend);
const removeListener = this.streamDispatcher.onPacket(async (e) => { const disposableList = new DisposableList();
disposableList.add(this.packetDispatcher.onPacket(async (e) => {
e.handled = true; e.handled = true;
const { packet } = e; const { packet } = e;
@ -89,12 +83,8 @@ export class WebAdb {
resolver.resolve(); resolver.resolve();
break; break;
case AdbCommand.Auth: case AdbCommand.Auth:
if (packet.arg0 !== AdbAuthType.Token) {
throw new Error('Unknown auth type');
}
const authPacket = await authHandler.tryNextAuth(e.packet); const authPacket = await authHandler.tryNextAuth(e.packet);
await this.streamDispatcher.sendPacket(authPacket); await this.packetDispatcher.sendPacket(authPacket);
break; break;
case AdbCommand.Close: case AdbCommand.Close:
// Last connection was interrupted // Last connection was interrupted
@ -104,17 +94,26 @@ export class WebAdb {
throw new Error('Device not in correct state. Reconnect your device and try again'); throw new Error('Device not in correct state. Reconnect your device and try again');
} }
} catch (e) { } catch (e) {
await this.dispose();
resolver.reject(e); resolver.reject(e);
} }
}); }));
await this.streamDispatcher.sendPacket(new AdbPacket(AdbCommand.Connect, version, 0x100000, `host::features=${features}`)); disposableList.add(this.packetDispatcher.onReceiveError(e => {
resolver.reject(e);
}));
await this.packetDispatcher.sendPacket(
AdbCommand.Connect,
version,
0x100000,
`host::features=${features}`
);
try { try {
await resolver.promise; await resolver.promise;
this._connected = true;
} finally { } finally {
removeListener(); disposableList.dispose();
} }
} }
@ -151,9 +150,9 @@ export class WebAdb {
} }
} }
public async shell(command: string, ...args: string[]): Promise<string>; public shell(command: string, ...args: string[]): Promise<string>;
public async shell(): Promise<AdbStream>; public shell(): Promise<AdbStream>;
public async shell(command?: string, ...args: string[]): Promise<AdbStream | string> { public shell(command?: string, ...args: string[]): Promise<AdbStream | string> {
if (!command) { if (!command) {
return this.createStream('shell:'); return this.createStream('shell:');
} else { } else {
@ -161,16 +160,35 @@ export class WebAdb {
} }
} }
public async tcpip(port = 5555): Promise<string> { public async getDaemonTcpAddresses(): Promise<string[]> {
const propAddr = (await this.shell('getprop', 'service.adb.listen_addrs')).trim();
if (propAddr) {
return propAddr.split(',');
}
let port = (await this.shell('getprop', 'service.adb.tcp.port')).trim();
if (port) {
return [`0.0.0.0:${port}`];
}
port = (await this.shell('getprop', 'persist.adb.tcp.port')).trim();
if (port) {
return [`0.0.0.0:${port}`];
}
return [];
}
public setDaemonTcpPort(port = 5555): Promise<string> {
return this.createStreamAndReadAll(`tcpip:${port}`); return this.createStreamAndReadAll(`tcpip:${port}`);
} }
public usb(): Promise<string> { public disableDaemonTcp(): Promise<string> {
return this.createStreamAndReadAll('usb:'); return this.createStreamAndReadAll('usb:');
} }
public async createStream(payload: string): Promise<AdbStream> { public async createStream(service: string): Promise<AdbStream> {
return this.streamDispatcher.createStream(payload); return this.packetDispatcher.createStream(service);
} }
public async createStreamAndReadAll(payload: string): Promise<string> { public async createStreamAndReadAll(payload: string): Promise<string> {
@ -179,7 +197,7 @@ export class WebAdb {
} }
public async dispose() { public async dispose() {
await this.streamDispatcher.dispose(); await this.packetDispatcher.dispose();
await this._transportation.dispose(); await this.backend.dispose();
} }
} }

110
packages/adb/src/auth.ts Normal file
View file

@ -0,0 +1,110 @@
import { AdbBackend, AdbKeyIterator } from './backend';
import { encodeBase64 } from './base64';
import { calculatePublicKey, sign } from './crypto';
import { AdbCommand, AdbPacket } from './packet';
export enum AdbAuthType {
Token = 1,
Signature = 2,
PublicKey = 3,
}
export interface AdbAuthenticator {
tryAuth(packet: AdbPacket): Promise<AdbPacket | undefined>;
}
export interface AdbAuthenticatorConstructor {
new(backend: AdbBackend): AdbAuthenticator;
}
export class AdbSignatureAuthenticator implements AdbAuthenticator {
private readonly backend: AdbBackend;
private readonly iterator: AdbKeyIterator;
public constructor(backend: AdbBackend) {
this.backend = backend;
this.iterator = backend.iterateKeys();
}
public async tryAuth(packet: AdbPacket): Promise<AdbPacket | undefined> {
if (packet.arg0 !== AdbAuthType.Token) {
return undefined;
}
const next = await this.iterator.next();
if (next.done) {
return undefined;
}
const signature = sign(next.value, packet.payload!);
return new AdbPacket(
this.backend,
AdbCommand.Auth,
AdbAuthType.Signature,
0,
signature
);
}
}
export class AdbPublicKeyAuthenticator implements AdbAuthenticator {
private backend: AdbBackend;
public constructor(backend: AdbBackend) {
this.backend = backend;
}
public async tryAuth(): Promise<AdbPacket> {
let privateKey: ArrayBuffer;
const iterator = this.backend.iterateKeys();
const next = await iterator.next();
if (!next.done) {
privateKey = next.value;
} else {
privateKey = await this.backend.generateKey();
}
const publicKey = calculatePublicKey(privateKey);
return new AdbPacket(
this.backend,
AdbCommand.Auth,
AdbAuthType.PublicKey,
0,
// adbd needs the extra null character
encodeBase64(publicKey) + '\0'
);
}
}
export const AdbDefaultAuthenticators: AdbAuthenticatorConstructor[] = [
AdbSignatureAuthenticator,
AdbPublicKeyAuthenticator
];
export class AdbAuthenticationHandler {
public readonly authenticators: readonly AdbAuthenticator[];
private index = 0;
public constructor(
authenticators: readonly AdbAuthenticatorConstructor[],
backend: AdbBackend
) {
this.authenticators = authenticators.map(Constructor => new Constructor(backend));
}
public async tryNextAuth(packet: AdbPacket): Promise<AdbPacket> {
while (this.index < this.authenticators.length) {
const result = await this.authenticators[this.index].tryAuth(packet);
if (result) {
return result;
}
this.index += 1;
}
throw new Error('Cannot authenticate with device');
}
}

View file

@ -0,0 +1,23 @@
import { Event } from '@yume-chan/event';
export type AdbKeyIterator = Iterator<ArrayBuffer> | AsyncIterator<ArrayBuffer>;
export interface AdbBackend {
readonly name: string | undefined;
readonly onDisconnected: Event<void>;
iterateKeys(): AdbKeyIterator;
generateKey(): ArrayBuffer | Promise<ArrayBuffer>;
encodeUtf8(input: string): ArrayBuffer;
decodeUtf8(buffer: ArrayBuffer): string;
write(buffer: ArrayBuffer): void | Promise<void>;
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
dispose(): void | Promise<void>;
}

159
packages/adb/src/base64.ts Normal file
View file

@ -0,0 +1,159 @@
interface Base64CharRange {
start: number;
length: number;
end: number;
offset: number;
}
let ranges: Base64CharRange[] = [];
let chars: string[] = [];
let offset = 0;
function addRange(start: string, end: string) {
const startCharCode = start.charCodeAt(0);
const endCharCode = end.charCodeAt(0);
const length = endCharCode - startCharCode + 1;
for (let i = startCharCode; i <= endCharCode; i++) {
chars.push(String.fromCharCode(i));
}
ranges.push({
start: startCharCode,
length: length,
end: endCharCode,
offset: startCharCode - offset,
});
offset += length;
}
addRange('A', 'Z');
addRange('a', 'z');
addRange('0', '9');
addRange('+', '+');
addRange('/', '/');
ranges = ranges.sort((a, b) => a.end - b.end);
function toValue(char: string): number {
const charCode = char.charCodeAt(0);
for (const range of ranges) {
if (charCode <= range.end) {
return charCode - range.offset;
}
}
throw new Error('Unknown Character');
}
export function encodeBase64(buffer: ArrayBuffer): string {
const array = new Uint8Array(buffer);
const length = buffer.byteLength;
const remainder = length % 3;
let result = '';
let i = 0;
for (; i < length - remainder;) {
// aaaaaabb
const x = array[i];
i += 1;
// bbbbcccc
const y = array[i];
i += 1;
// ccdddddd
const z = array[i];
i += 1;
result += chars[x >> 2];
result += chars[((x & 0b11) << 4) | (y >> 4)];
result += chars[((y & 0b1111) << 2) | (z >> 6)];
result += chars[z & 0b111111];
}
if (remainder === 1) {
// aaaaaabb
const x = array[i];
result += chars[x >> 2];
result += chars[((x & 0b11) << 4)];
result += '==';
} else if (remainder === 2) {
// aaaaaabb
const x = array[i];
i += 1;
// bbbbcccc
const y = array[i];
result += chars[x >> 2];
result += chars[((x & 0b11) << 4) | (y >> 4)];
result += chars[((y & 0b1111) << 2)];
result += '=';
}
return result;
}
export function decodeBase64(input: string): ArrayBuffer {
let padding: number;
if (input[input.length - 2] === '=') {
padding = 2;
} else if (input[input.length - 1] === '=') {
padding = 1;
} else {
padding = 0;
}
const result = new Uint8Array(input.length / 4 * 3 - padding);
let sIndex = 0;
let dIndex = 0;
while (sIndex < input.length - (padding !== 0 ? 4 : 0)) {
const a = toValue(input[sIndex]);
sIndex += 1;
const b = toValue(input[sIndex]);
sIndex += 1;
const c = toValue(input[sIndex]);
sIndex += 1;
const d = toValue(input[sIndex]);
sIndex += 1;
result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4);
dIndex += 1;
result[dIndex] = ((b & 0b1111) << 4) | ((c & 0b11_1100) >> 2);
dIndex += 1;
result[dIndex] = ((c & 0b11) << 6) | d;
dIndex += 1;
}
if (padding === 1) {
const a = toValue(input[sIndex]);
sIndex += 1;
const b = toValue(input[sIndex]);
sIndex += 1;
const c = toValue(input[sIndex]);
result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4);
dIndex += 1;
result[dIndex] = ((b & 0b1111) << 4) | ((c & 0b11_1100) >> 2);
} else if (padding === 2) {
const a = toValue(input[sIndex]);
sIndex += 1;
const b = toValue(input[sIndex]);
result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4);
}
return result.buffer;
}

View file

@ -113,21 +113,7 @@ export function modInverse(a: number, m: number) {
return (y % m + m) % m return (y % m + m) % m
} }
export async function generateKey(): Promise<[privateKey: ArrayBuffer, publicKey: ArrayBuffer]> { export function calculatePublicKey(privateKey: ArrayBuffer): ArrayBuffer {
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
// 65537
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: 'SHA-1',
},
true,
['sign', 'verify']
);
const privateKey = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
// Android has its own public key generation algorithm // Android has its own public key generation algorithm
// See https://github.com/aosp-mirror/platform_system_core/blob/e5c9bbd45381d7bd72fef232d1c6668946253ac8/libcrypto_utils/android_pubkey.cpp#L111 // See https://github.com/aosp-mirror/platform_system_core/blob/e5c9bbd45381d7bd72fef232d1c6668946253ac8/libcrypto_utils/android_pubkey.cpp#L111
@ -170,7 +156,7 @@ export async function generateKey(): Promise<[privateKey: ArrayBuffer, publicKey
// exponent // exponent
publicKeyView.setUint32(8 + 256 + 256, 65537, true); publicKeyView.setUint32(8 + 256 + 256, 65537, true);
return [privateKey, publicKey]; return publicKey;
} }
export const Sha1DigestLength = 20; export const Sha1DigestLength = 20;

View file

@ -0,0 +1,7 @@
export * from './adb';
export * from './auth';
export * from './backend';
export * from './base64';
export * from './crypto';
export * from './packet';
export * from './stream';

149
packages/adb/src/packet.ts Normal file
View file

@ -0,0 +1,149 @@
import { AdbBackend } from './backend';
namespace Assert {
export function command(command: string): void {
const length = command.length;
if (length !== 4) {
throw new Error(`AdbPacket: command.length mismatch. Expected 4, but got ${length}`);
}
}
export function magic(view: DataView): void {
const expected = view.getUint32(0) ^ 0xFFFFFFFF;
const actual = view.getInt32(20);
if (expected !== actual) {
throw new Error(`AdbPacket: magic number mismatch. Expected ${expected}, but got ${actual}`);
}
}
}
export enum AdbCommand {
Connect = 'CNXN',
Auth = 'AUTH',
OK = 'OKAY',
Close = 'CLSE',
Write = 'WRTE',
Open = 'OPEN',
}
export class AdbPacket {
public static async parse(backend: AdbBackend): Promise<AdbPacket> {
let buffer = await backend.read(24);
if (buffer.byteLength !== 24) {
// Maybe it's a payload from last connection.
// Ignore and try again
buffer = await backend.read(24);
}
const view = new DataView(buffer);
Assert.magic(view);
const command = backend.decodeUtf8(buffer.slice(0, 4));
const arg0 = view.getUint32(4, true);
const arg1 = view.getUint32(8, true);
const payloadLength = view.getUint32(12, true);
let payload: ArrayBuffer | undefined;
if (payloadLength !== 0) {
payload = await backend.read(payloadLength);
}
return new AdbPacket(backend, command, arg0, arg1, payload);
}
public static send(
backend: AdbBackend,
command: AdbCommand,
arg0: number,
arg1: number,
payload?: string | ArrayBuffer
): Promise<void> {
const packet = new AdbPacket(backend, command, arg0, arg1, payload);
return packet.send();
}
private backend: AdbBackend;
public command: string;
public arg0: number;
public arg1: number;
private _payloadLength!: number;
public get payloadLength(): number { return this._payloadLength; }
private _payload: ArrayBuffer | undefined;
public get payload(): ArrayBuffer | undefined { return this._payload; }
public set payload(value: ArrayBuffer | undefined) {
if (value !== undefined) {
this._payloadLength = value.byteLength;
this._payload = value;
} else {
this._payloadLength = 0;
this._payload = undefined;
}
this._payloadString = undefined;
}
private _payloadString: string | undefined;
public get payloadString(): string | undefined {
if (!this._payload) {
return undefined;
}
if (!this._payloadString) {
this._payloadString = this.backend.decodeUtf8(this._payload);
}
return this._payloadString;
}
public set payloadString(value: string | undefined) {
if (value !== undefined) {
this.payload = this.backend.encodeUtf8(value);
this._payloadString = value;
} else {
this.payload = undefined;
}
}
public constructor(
backend: AdbBackend,
command: string,
arg0: number,
arg1: number,
payload?: string | ArrayBuffer
) {
this.backend = backend;
Assert.command(command);
this.command = command;
this.arg0 = arg0;
this.arg1 = arg1;
if (typeof payload === "string") {
this.payloadString = payload;
} else {
this.payload = payload;
}
}
public async send(): Promise<void> {
const buffer = new ArrayBuffer(24);
const array = new Uint8Array(buffer);
array.set(new Uint8Array(this.backend.encodeUtf8(this.command)));
const view = new DataView(buffer);
view.setUint32(4, this.arg0, true);
view.setUint32(8, this.arg1, true);
view.setUint32(12, this.payloadLength, true);
view.setUint32(16, /* checksum */ 0, true);
view.setUint32(20, /* magic */ view.getUint32(0, true) ^ 0xFFFFFFFF, true);
await this.backend.write(buffer);
if (this.payload) {
await this.backend.write(this.payload);
}
}
}

View file

@ -1,65 +1,68 @@
import AsyncOperationManager, { PromiseResolver } from '@yume-chan/async-operation-manager'; import AsyncOperationManager, { PromiseResolver } from '@yume-chan/async-operation-manager';
import { AutoDisposable, Disposable, Event, EventEmitter } from '@yume-chan/event'; import { AutoDisposable, Disposable, Event, EventEmitter } from '@yume-chan/event';
import { decode } from './decode';
import { AdbPacket } from './packet'; import { AdbPacket } from './packet';
import { WebAdbTransportation } from './transportation'; import { AdbBackend } from './backend';
import { AdbCommand } from './webadb'; import { AdbCommand } from './packet';
class AutoResetEvent { class AutoResetEvent implements Disposable {
private _list: PromiseResolver<void>[] = []; private readonly list: PromiseResolver<void>[] = [];
private _blocking: boolean = false; private blocking: boolean = false;
public wait(): Promise<void> { public wait(): Promise<void> {
if (!this._blocking) { if (!this.blocking) {
this._blocking = true; this.blocking = true;
if (this._list.length === 0) { if (this.list.length === 0) {
return Promise.resolve(); return Promise.resolve();
} }
} }
const resolver = new PromiseResolver<void>(); const resolver = new PromiseResolver<void>();
this._list.push(resolver); this.list.push(resolver);
return resolver.promise; return resolver.promise;
} }
public notify() { public notify() {
if (this._list.length !== 0) { if (this.list.length !== 0) {
this._list.pop()!.resolve(); this.list.pop()!.resolve();
} else { } else {
this._blocking = false; this.blocking = false;
} }
} }
public dispose() {
for (const item of this.list) {
item.reject(new Error('The AutoResetEvent has been disposed'));
}
this.list.length = 0;
}
} }
export class AdbStreamController extends AutoDisposable { export class AdbStreamController extends AutoDisposable {
private dispatcher: AdbStreamDispatcher; private readonly writeLock = this.addDisposable(new AutoResetEvent());
private writeLock = new AutoResetEvent(); public readonly dispatcher: AdbPacketDispatcher;
private _localId: number; public readonly localId: number;
public get localId() { return this._localId; }
private _remoteId: number; public readonly remoteId: number;
public get remoteId() { return this._remoteId; }
public onDataEvent = this.addDisposable(new EventEmitter<ArrayBuffer>()); public readonly onDataEvent = this.addDisposable(new EventEmitter<ArrayBuffer>());
public onCloseEvent = this.addDisposable(new EventEmitter<void>()); public readonly onCloseEvent = this.addDisposable(new EventEmitter<void>());
public constructor(localId: number, remoteId: number, dispatcher: AdbStreamDispatcher) { public constructor(localId: number, remoteId: number, dispatcher: AdbPacketDispatcher) {
super(); super();
this._localId = localId; this.localId = localId;
this._remoteId = remoteId; this.remoteId = remoteId;
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
dispatcher.addStreamController(this);
} }
public async write(data: ArrayBuffer): Promise<void> { public async write(data: ArrayBuffer): Promise<void> {
await this.writeLock.wait(); await this.writeLock.wait();
await this.dispatcher.sendPacket(new AdbPacket(AdbCommand.Write, this.localId, this.remoteId, data)); await this.dispatcher.sendPacket(AdbCommand.Write, this.localId, this.remoteId, data);
} }
public ack() { public ack() {
@ -67,49 +70,75 @@ export class AdbStreamController extends AutoDisposable {
} }
public close() { public close() {
return this.dispatcher.sendPacket(new AdbPacket(AdbCommand.Close, this.localId, this.remoteId)); return this.dispatcher.sendPacket(AdbCommand.Close, this.localId, this.remoteId);
} }
} }
export interface OnAdbPacketEventArgs { export interface AdbPacketArrivedEventArgs {
handled: boolean; handled: boolean;
packet: AdbPacket; packet: AdbPacket;
} }
export class AdbStreamDispatcher implements Disposable { export class AdbPacketDispatcher extends AutoDisposable {
private transportation: WebAdbTransportation; public readonly backend: AdbBackend;
// ADB requires stream id to start from 1 // ADB requires stream id to start from 1
// (0 means open failed) // (0 means open failed)
private initializers = new AsyncOperationManager(1); private readonly initializers = new AsyncOperationManager(1);
private streams = new Map<number, AdbStreamController>(); private readonly streams = new Map<number, AdbStreamController>();
private onPacketEvent = new EventEmitter<OnAdbPacketEventArgs>(); private readonly onPacketEvent = this.addDisposable(new EventEmitter<AdbPacketArrivedEventArgs>());
public get onPacket() { return this.onPacketEvent.event; } public readonly onPacket = this.onPacketEvent.event;
private readonly onReceiveErrorEvent = this.addDisposable(new EventEmitter<Error>());
public readonly onReceiveError = this.onReceiveErrorEvent.event;
private _running = true; private _running = true;
public get running() { return this._running; } public get running() { return this._running; }
public constructor(transportation: WebAdbTransportation) { public constructor(backend: AdbBackend) {
this.transportation = transportation; super();
this.backend = backend;
this.receiveLoop(); this.receiveLoop();
} }
public async createStream(payload: string): Promise<AdbStream> { public async createStream(service: string): Promise<AdbStream> {
const [localId, initializer] = this.initializers.add<number>(); const [localId, initializer] = this.initializers.add<number>();
await this.sendPacket(new AdbPacket(AdbCommand.Open, localId, 0, payload)); await this.sendPacket(AdbCommand.Open, localId, 0, service);
const remoteId = await initializer; const remoteId = await initializer;
return new AdbStream(localId, remoteId, this); const controller = new AdbStreamController(localId, remoteId, this);
}
public addStreamController(controller: AdbStreamController) {
this.streams.set(controller.localId, controller); this.streams.set(controller.localId, controller);
return new AdbStream(controller);
} }
public sendPacket(packet: AdbPacket): Promise<void> { public sendPacket(packet: AdbPacket): Promise<void>;
return packet.writeTo(this.transportation); public sendPacket(
command: AdbCommand,
arg0: number,
arg1: number,
payload?: string | ArrayBuffer
): Promise<void>;
public sendPacket(
packetOrCommand: AdbPacket | AdbCommand,
arg0?: number,
arg1?: number,
payload?: string | ArrayBuffer
): Promise<void> {
if (arguments.length === 1) {
return (packetOrCommand as AdbPacket).send();
} else {
return AdbPacket.send(
this.backend,
packetOrCommand as AdbCommand,
arg0 as number,
arg1 as number,
payload
);
}
} }
public async dispose() { public async dispose() {
@ -121,23 +150,14 @@ export class AdbStreamDispatcher implements Disposable {
stream.dispose(); stream.dispose();
} }
this.streams.clear(); this.streams.clear();
}
private async receivePacket() { super.dispose();
const header = await this.transportation.read(24);
const packet = AdbPacket.parse(header);
if (packet.payloadLength !== 0) {
packet.payload = await this.transportation.read(packet.payloadLength);
}
return packet;
} }
private async receiveLoop() { private async receiveLoop() {
while (this._running) { while (this._running) {
try { try {
const packet = await this.receivePacket(); const packet = await AdbPacket.parse(this.backend);
let handled = false; let handled = false;
switch (packet.command) { switch (packet.command) {
case AdbCommand.OK: case AdbCommand.OK:
@ -152,7 +172,7 @@ export class AdbStreamDispatcher implements Disposable {
// Last connection sent an OPEN to device, // Last connection sent an OPEN to device,
// device now sent OKAY to this connection // device now sent OKAY to this connection
// tell the device to close the stream // tell the device to close the stream
this.sendPacket(new AdbPacket(AdbCommand.Close, packet.arg1, packet.arg0)); this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
} }
break; break;
case AdbCommand.Close: case AdbCommand.Close:
@ -175,14 +195,14 @@ export class AdbStreamDispatcher implements Disposable {
case AdbCommand.Write: case AdbCommand.Write:
if (this.streams.has(packet.arg1)) { if (this.streams.has(packet.arg1)) {
this.streams.get(packet.arg1)!.onDataEvent.fire(packet.payload!); this.streams.get(packet.arg1)!.onDataEvent.fire(packet.payload!);
await this.sendPacket(new AdbPacket(AdbCommand.OK, packet.arg1, packet.arg0)); await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0);
handled = true; handled = true;
} }
break; break;
} }
if (!handled) { if (!handled) {
const args: OnAdbPacketEventArgs = { const args: AdbPacketArrivedEventArgs = {
handled: false, handled: false,
packet, packet,
} }
@ -197,7 +217,7 @@ export class AdbStreamDispatcher implements Disposable {
// ignore error // ignore error
} }
throw e; this.onReceiveErrorEvent.fire(e);
} }
} }
} }
@ -214,15 +234,15 @@ export class AdbStream {
public get onClose(): Event<void> { return this.controller.onCloseEvent.event; } public get onClose(): Event<void> { return this.controller.onCloseEvent.event; }
public constructor(localId: number, remoteId: number, dispatcher: AdbStreamDispatcher) { public constructor(controller: AdbStreamController) {
this.controller = new AdbStreamController(localId, remoteId, dispatcher); this.controller = controller;
} }
public async readAll(): Promise<string> { public async readAll(): Promise<string> {
const resolver = new PromiseResolver<string>(); const resolver = new PromiseResolver<string>();
let output = ''; let output = '';
this.onData((data) => { this.onData((data) => {
output += decode(data); output += this.controller.dispatcher.backend.decodeUtf8(data);
}); });
this.onClose(() => { this.onClose(() => {
resolver.resolve(output); resolver.resolve(output);

View file

@ -40,33 +40,44 @@
} }
}, },
"@fluentui/react-focus": { "@fluentui/react-focus": {
"version": "7.13.1", "version": "7.13.2",
"resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.13.2.tgz",
"integrity": "sha512-O61h/aC8ZZ0IEU0oqgBmrk2b/yNMBM0nghJ5IKwbGOD7s1bt/+FnAnJc+rmSD5Atrt7otPkMJma0yUu6qXqxHQ==", "integrity": "sha512-t5BKR5uZw8A9VFH0BpkKA25Bx0iJfz9cDqrdOzFCJl3tTcTZYnTYxr3SSuw7Psp2ApdYWcnOZI6frozS+AvMnA==",
"requires": { "requires": {
"@fluentui/keyboard-key": "^0.2.11", "@fluentui/keyboard-key": "^0.2.11",
"@uifabric/merge-styles": "^7.16.7", "@uifabric/merge-styles": "^7.17.0",
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"@uifabric/styling": "^7.15.1", "@uifabric/styling": "^7.15.2",
"@uifabric/utilities": "^7.29.1", "@uifabric/utilities": "^7.30.0",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@fluentui/react-icons": { "@fluentui/react-icons": {
"version": "0.2.1", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-0.2.2.tgz",
"integrity": "sha512-FfHgiFrOmQEoGtnfzoL/1L6jmWXcZ+5GHc++TktSs6q/2XXDbU7qjE9pOankoKV+pJXKGHUPCLuNfI8j2eFnbA==", "integrity": "sha512-pofZsHgbBVcyVr3I/LlbrX3P06NZnToxlH7cS9xy3LFcyrj629S5+iEXFdVk4O496MJ6qd4/FyxI/3Ru9UNlDg==",
"requires": { "requires": {
"@microsoft/load-themed-styles": "^1.10.26", "@microsoft/load-themed-styles": "^1.10.26",
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"@uifabric/utilities": "^7.29.1", "@uifabric/utilities": "^7.30.0",
"tslib": "^1.10.0"
}
},
"@fluentui/theme": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-0.1.1.tgz",
"integrity": "sha512-RwFv1YUKVeOuUNjLIO2SWBhqrwjUm1Ja8p0NLKInFd1UM/tAvwwZJtXpdzgz4SAQHxy1ceBLznzKkOiJv1+mBg==",
"requires": {
"@uifabric/merge-styles": "^7.17.0",
"@uifabric/set-version": "^7.0.22",
"@uifabric/utilities": "^7.30.0",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@microsoft/load-themed-styles": { "@microsoft/load-themed-styles": {
"version": "1.10.78", "version": "1.10.79",
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.78.tgz", "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.79.tgz",
"integrity": "sha512-ehoPzYqpoVNkKheGHR4B3wOax+5L745KeGN8m+N6Qm/yypmnahFMp0Jk8ilTvrAFNcZ4UQl8I2YRbTXLCGdP/g==" "integrity": "sha512-p4Ym7B2d8HT1K0VHAQkUw1SRmXQrko59lmqg/cr8ORClXIhoqc6Zt3m6I53olz4af2pXzpiI0ynHu6KmYYLOfQ=="
}, },
"@types/glob": { "@types/glob": {
"version": "7.1.3", "version": "7.1.3",
@ -149,31 +160,31 @@
} }
}, },
"@uifabric/foundation": { "@uifabric/foundation": {
"version": "7.8.1", "version": "7.8.2",
"resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.8.1.tgz", "resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.8.2.tgz",
"integrity": "sha512-66P4Y2hJosLTl/QXSD780aSnxVR+ySmlugjgLgJkAUHPai84Ztbblv08p07GjDyN7SVs1/NRK7k9u7joMz8h8g==", "integrity": "sha512-TQKJ73zcpnSaGSmwuCrL06q9zmOJhhBaOJn3bxENb9038lMwuoMikgTWWouD1C0woH8/J7lCxd/hcO4USe0MSw==",
"requires": { "requires": {
"@uifabric/merge-styles": "^7.16.7", "@uifabric/merge-styles": "^7.17.0",
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"@uifabric/styling": "^7.15.1", "@uifabric/styling": "^7.15.2",
"@uifabric/utilities": "^7.29.1", "@uifabric/utilities": "^7.30.0",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@uifabric/icons": { "@uifabric/icons": {
"version": "7.4.1", "version": "7.4.2",
"resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.4.1.tgz", "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.4.2.tgz",
"integrity": "sha512-KgCYshGmcbLPoIIQK+OuKucM39smDSjsz7O4Hnu61xZmK1mOBCbFtBlgJRMlINZr0d6IH0vU2yrkp8SXHF2h0A==", "integrity": "sha512-dd+IxpuQzso3ys2D873nOl/C2vvw3vHEDkWjJxIWKoaTqB7KoWMKf50Q7ObBei19ue/WgqKvTMOtKG1bgJP0bw==",
"requires": { "requires": {
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"@uifabric/styling": "^7.15.1", "@uifabric/styling": "^7.15.2",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@uifabric/merge-styles": { "@uifabric/merge-styles": {
"version": "7.16.7", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.17.0.tgz",
"integrity": "sha512-yOjCexa1/I6c7p4lsgKGWbzoljVidg4IxigbeLjcvACvTdOFoLXguxWBIXn31Ralo28Ih4GC6ykhUTBGfsNTXg==", "integrity": "sha512-UFG0pKqjz6iGcYhDFDSb2br5TEOys90mCaXeXPln8QrIc5NTJG1gWeO4znG6r65e23Ab/hBz+lmoyfYxgr3fZw==",
"requires": { "requires": {
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"tslib": "^1.10.0" "tslib": "^1.10.0"
@ -198,23 +209,24 @@
} }
}, },
"@uifabric/styling": { "@uifabric/styling": {
"version": "7.15.1", "version": "7.15.2",
"resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.15.1.tgz", "resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.15.2.tgz",
"integrity": "sha512-jNEKVEo77HBGXSMwILNqfTYzXOoPY9AMRHF2TViBodKsSd8EqaEh1Q5yDj7N76Db1bgyIWW0xgjp6W+9ZaMA0w==", "integrity": "sha512-J1S1i87+Re1258saDFMIbJps8w9mtL/lxFJLa7cCwt642xpbwx9L/11mTT9zBrf2yUYLK+KxCrAOYQS92uY01Q==",
"requires": { "requires": {
"@fluentui/theme": "^0.1.1",
"@microsoft/load-themed-styles": "^1.10.26", "@microsoft/load-themed-styles": "^1.10.26",
"@uifabric/merge-styles": "^7.16.7", "@uifabric/merge-styles": "^7.17.0",
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"@uifabric/utilities": "^7.29.1", "@uifabric/utilities": "^7.30.0",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@uifabric/utilities": { "@uifabric/utilities": {
"version": "7.29.1", "version": "7.30.0",
"resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.29.1.tgz", "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.30.0.tgz",
"integrity": "sha512-LaXkxFCn1SQcD1HvhX8PG1jjnK2DG4xKD7PE95rt0+QMvI+10q9qZv6dVhPD2kLCf+8S06elgBE6xbQnmTQFNg==", "integrity": "sha512-NO+XkCBsaxVzOA+JytgSGAK3ihPDG5XLPQQhFbAXSEc5eC6qzcSY6JswRPy3hC1DIWicTUh+iTrDbb+x2vZ8Wg==",
"requires": { "requires": {
"@uifabric/merge-styles": "^7.16.7", "@uifabric/merge-styles": "^7.17.0",
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"tslib": "^1.10.0" "tslib": "^1.10.0"
@ -743,6 +755,15 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true "dev": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
} }
} }
}, },
@ -1346,9 +1367,9 @@
"dev": true "dev": true
}, },
"csstype": { "csstype": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
"integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==", "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==",
"dev": true "dev": true
}, },
"cyclist": { "cyclist": {
@ -1724,9 +1745,9 @@
"dev": true "dev": true
}, },
"eventemitter3": { "eventemitter3": {
"version": "4.0.5", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.5.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.6.tgz",
"integrity": "sha512-QR0rh0YiPuxuDQ6+T9GAO/xWTExXpxIes1Nl9RykNGTnE1HJmkuEfxJH9cubjIOQZ/GH4qNBR4u8VSHaKiWs4g==", "integrity": "sha512-s3GJL04SQoM+gn2c14oyqxvZ3Pcq7cduSDqy3sBFXx6UPSUmgVYwQM9zwkTn9je0lrfg0gHEwR42pF3Q2dCQkQ==",
"dev": true "dev": true
}, },
"events": { "events": {
@ -2670,12 +2691,12 @@
"dev": true "dev": true
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3.0.0"
} }
}, },
"icss-utils": { "icss-utils": {
@ -3603,23 +3624,35 @@
"dev": true "dev": true
}, },
"office-ui-fabric-react": { "office-ui-fabric-react": {
"version": "7.130.0", "version": "7.131.0",
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.130.0.tgz", "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.131.0.tgz",
"integrity": "sha512-HV5LFSZAp8t3uJrGXhwPc/KHFPSGOka3BehRQnzN+7Ke++Abmynq7sIJSj36sNPa13uSiESfCFf30eY5ajnqPQ==", "integrity": "sha512-KweqH/OFq78z/W64Z1cSHxh8CBcrVOuYW5MwDtRIGlPorgOAYeS+z/Kx6cqJhlZIwRHLCrIgTKZ032vYXXtOrA==",
"requires": { "requires": {
"@fluentui/date-time-utilities": "^7.6.0", "@fluentui/date-time-utilities": "^7.6.0",
"@fluentui/react-focus": "^7.13.1", "@fluentui/react-focus": "^7.13.2",
"@fluentui/react-icons": "^0.2.1", "@fluentui/react-icons": "^0.2.2",
"@microsoft/load-themed-styles": "^1.10.26", "@microsoft/load-themed-styles": "^1.10.26",
"@uifabric/foundation": "^7.8.1", "@uifabric/foundation": "^7.8.2",
"@uifabric/icons": "^7.4.1", "@uifabric/icons": "^7.4.2",
"@uifabric/merge-styles": "^7.16.7", "@uifabric/merge-styles": "^7.17.0",
"@uifabric/react-hooks": "^7.9.1", "@uifabric/react-hooks": "^7.10.0",
"@uifabric/set-version": "^7.0.22", "@uifabric/set-version": "^7.0.22",
"@uifabric/styling": "^7.15.1", "@uifabric/styling": "^7.15.2",
"@uifabric/utilities": "^7.29.1", "@uifabric/utilities": "^7.30.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"tslib": "^1.10.0" "tslib": "^1.10.0"
},
"dependencies": {
"@uifabric/react-hooks": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/@uifabric/react-hooks/-/react-hooks-7.10.0.tgz",
"integrity": "sha512-zbxsFrXexfj5bPDBDh4BNFZj8vs8Jf6jmvZYRk+r/ZtvfceDyjuWcF+fYc2fjCon6pDfL5N8/S5Bc4NRw6sfvw==",
"requires": {
"@uifabric/set-version": "^7.0.22",
"@uifabric/utilities": "^7.30.0",
"tslib": "^1.10.0"
}
}
} }
}, },
"on-finished": { "on-finished": {
@ -4146,6 +4179,15 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true "dev": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
} }
} }
}, },
@ -4810,17 +4852,6 @@
"loader-utils": "^2.0.0", "loader-utils": "^2.0.0",
"schema-utils": "^2.7.0", "schema-utils": "^2.7.0",
"source-map": "^0.6.1" "source-map": "^0.6.1"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
} }
}, },
"source-map-resolve": { "source-map-resolve": {

View file

@ -34,7 +34,8 @@
"dependencies": { "dependencies": {
"@fluentui/react": "7.130.0", "@fluentui/react": "7.130.0",
"@uifabric/react-hooks": "7.9.1", "@uifabric/react-hooks": "7.9.1",
"@yume-chan/webadb": "^0.0.1", "@yume-chan/adb": "^0.0.1",
"@yume-chan/adb-webusb": "^0.0.1",
"react": "16.13.1", "react": "16.13.1",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",

View file

@ -1,13 +1,14 @@
import { DefaultButton, Dialog, DialogFooter, DialogType, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react'; import { DefaultButton, Dialog, DialogFooter, DialogType, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
import { useBoolean } from '@uifabric/react-hooks'; import { useBoolean } from '@uifabric/react-hooks';
import { WebAdb, WebUsbTransportation } from '@yume-chan/webadb'; import { Adb } from '@yume-chan/adb';
import { WebUsbAdbBackend } from '@yume-chan/adb-webusb';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import withDisplayName from './withDisplayName'; import withDisplayName from './withDisplayName';
interface ConnectProps { interface ConnectProps {
device: WebAdb | undefined; device: Adb | undefined;
onDeviceChange: (device: WebAdb | undefined) => void; onDeviceChange: (device: Adb | undefined) => void;
} }
export default withDisplayName('Connect', ({ export default withDisplayName('Connect', ({
@ -21,12 +22,17 @@ export default withDisplayName('Connect', ({
const connect = useCallback(async () => { const connect = useCallback(async () => {
try { try {
const transportation = await WebUsbTransportation.pickDevice(); const backend = await WebUsbAdbBackend.pickDevice();
if (transportation) { if (backend) {
const device = new WebAdb(transportation); const device = new Adb(backend);
setConnecting(true); try {
await device.connect(); setConnecting(true);
onDeviceChange(device); await device.connect();
onDeviceChange(device);
} catch (e) {
device.dispose();
throw e;
}
} }
} catch (e) { } catch (e) {
setErrorMessage(e.message); setErrorMessage(e.message);
@ -47,17 +53,9 @@ export default withDisplayName('Connect', ({
}, [device]); }, [device]);
useEffect(() => { useEffect(() => {
function handler(e: USBConnectionEvent) { return device?.onDisconnected(() => {
if (e.device.productName === device?.name) { onDeviceChange(undefined);
onDeviceChange(undefined); });
}
}
window.navigator.usb.addEventListener('disconnect', handler);
return () => {
window.navigator.usb.removeEventListener('disconnect', handler);
};
}, [device, onDeviceChange]); }, [device, onDeviceChange]);
return ( return (

View file

@ -1,4 +1,4 @@
import { WebAdb } from '@yume-chan/webadb'; import { Adb } from '@yume-chan/adb';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from 'xterm-addon-fit';
@ -13,7 +13,7 @@ const containerStyle: CSSProperties = {
}; };
export interface ShellProps { export interface ShellProps {
device: WebAdb | undefined; device: Adb | undefined;
visible: boolean; visible: boolean;
} }

View file

@ -1,6 +1,6 @@
import { Label, Link, MessageBar, Nav, PrimaryButton, Separator, Stack, StackItem, Text, TextField } from '@fluentui/react'; import { Label, Link, MessageBar, Nav, PrimaryButton, Separator, Stack, StackItem, Text, TextField } from '@fluentui/react';
import { useId } from '@uifabric/react-hooks'; import { useId } from '@uifabric/react-hooks';
import { WebAdb } from '@yume-chan/webadb'; import { Adb } from '@yume-chan/adb';
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons'; import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@ -14,12 +14,12 @@ initializeIcons();
function App(): JSX.Element | null { function App(): JSX.Element | null {
const location = useLocation(); const location = useLocation();
const [device, setDevice] = useState<WebAdb | undefined>(); const [device, setDevice] = useState<Adb | undefined>();
const [tcpPort, setTcpPort] = useState<number | undefined>(); const [tcpPort, setTcpAddresses] = useState<string[] | undefined>();
useEffect(() => { useEffect(() => {
if (!device) { if (!device) {
setTcpPort(undefined); setTcpAddresses(undefined);
} }
}, [device]); }, [device]);
const queryTcpPort = useCallback(async () => { const queryTcpPort = useCallback(async () => {
@ -27,8 +27,8 @@ function App(): JSX.Element | null {
return; return;
} }
const result = await device.shell('getprop service.adb.tcp.port'); const result = await device.getDaemonTcpAddresses();
setTcpPort(Number.parseInt(result, 10)); setTcpAddresses(result);
}, [device]); }, [device]);
const [tcpPortValue, setTcpPortValue] = useState('5555'); const [tcpPortValue, setTcpPortValue] = useState('5555');
@ -38,7 +38,7 @@ function App(): JSX.Element | null {
return; return;
} }
const result = await device.tcpip(Number.parseInt(tcpPortValue, 10)); const result = await device.setDaemonTcpPort(Number.parseInt(tcpPortValue, 10));
console.log(result); console.log(result);
}, [device, tcpPortValue]); }, [device, tcpPortValue]);
@ -47,7 +47,7 @@ function App(): JSX.Element | null {
return; return;
} }
const result = await device.usb(); const result = await device.disableDaemonTcp();
console.log(result); console.log(result);
}, [device]); }, [device]);
@ -132,8 +132,8 @@ function App(): JSX.Element | null {
</StackItem> </StackItem>
<StackItem> <StackItem>
{tcpPort !== undefined && {tcpPort !== undefined &&
(tcpPort !== 0 (tcpPort.length !== 0
? `Enabled at port ${tcpPort}` ? `Enabled at ${tcpPort.join(', ')}`
: 'Disabled')} : 'Disabled')}
</StackItem> </StackItem>
</Stack> </Stack>
@ -161,7 +161,7 @@ function App(): JSX.Element | null {
<StackItem> <StackItem>
<PrimaryButton <PrimaryButton
text="Disable" text="Disable"
disabled={!device || tcpPort === undefined || tcpPort === 0} disabled={!device || tcpPort === undefined || tcpPort.length === 0}
onClick={disableTcp} onClick={disableTcp}
/> />
</StackItem> </StackItem>

21
packages/event/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Simon Chan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,78 +0,0 @@
import base64Encode from './base64';
import { generateKey, sign } from './crypto';
import { stringToArrayBuffer } from './decode';
import { AdbPacket } from './packet';
import { AdbAuthType, AdbCommand } from './webadb';
export interface AdbAuthMethod {
tryAuth(packet: AdbPacket): Promise<AdbPacket | undefined>;
}
const PublicKeyStorageKey = 'public-key';
const PrivateKeyStorageKey = 'private-key';
export class SignatureAuthMethod implements AdbAuthMethod {
private keys: string[] = [];
private index = 0;
public constructor() {
const privateKeyBase64 = window.localStorage.getItem(PrivateKeyStorageKey);
if (privateKeyBase64) {
this.keys.push(privateKeyBase64);
}
}
public async tryAuth(packet: AdbPacket): Promise<AdbPacket | undefined> {
if (this.index === this.keys.length) {
return undefined
}
const privateKey = stringToArrayBuffer(atob(this.keys[this.index]));
this.index += 1;
const signature = sign(privateKey, packet.payload!);
return new AdbPacket(AdbCommand.Auth, AdbAuthType.Signature, 0, signature);
}
}
export const PublicKeyAuthMethod: AdbAuthMethod = {
async tryAuth(): Promise<AdbPacket> {
let publicKeyBase64 = window.localStorage.getItem(PublicKeyStorageKey);
if (!publicKeyBase64) {
const [privateKey, publicKey] = await generateKey();
publicKeyBase64 = base64Encode(publicKey);
window.localStorage.setItem(PublicKeyStorageKey, publicKeyBase64);
const privateKeyBase64 = base64Encode(privateKey);
window.localStorage.setItem(PrivateKeyStorageKey, privateKeyBase64);
}
// adbd needs the extra null character
return new AdbPacket(AdbCommand.Auth, AdbAuthType.PublicKey, 0, publicKeyBase64 + '\0');
}
}
export class AdbAuthHandler {
public readonly methods: readonly AdbAuthMethod[];
private index = 0;
public constructor(methods: readonly AdbAuthMethod[]) {
this.methods = methods;
}
public async tryNextAuth(packet: AdbPacket): Promise<AdbPacket> {
while (this.index < this.methods.length) {
const result = await this.methods[this.index].tryAuth(packet);
if (result) {
return result;
}
this.index += 1;
}
throw new Error('Cannot authenticate with device');
}
}

View file

@ -1,59 +0,0 @@
let characterSet: string[] = [];
const pairs = [
['A', 'Z'],
['a', 'z'],
['0', '9'],
].map(pair => pair.map(character => character.charCodeAt(0)));
for (const [begin, end] of pairs) {
for (let i = begin; i <= end; i += 1) {
characterSet.push(String.fromCharCode(i));
}
}
characterSet.push('+', '/');
export default function base64Encode(buffer: ArrayBuffer) {
const array = new Uint8Array(buffer);
const length = buffer.byteLength;
const remainder = length % 3;
let result = '';
for (let i = 0; i < length - remainder; i += 3) {
// aaaaaabb
const x = array[i];
// bbbbcccc
const y = array[i + 1];
// ccdddddd
const z = array[i + 2];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2) | (z >> 6);
const d = z & 0b111111;
result += characterSet[a] + characterSet[b] + characterSet[c] + characterSet[d];
}
if (remainder === 1) {
// aaaaaabb
const x = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4);
result += characterSet[a] + characterSet[b] + '==';
} else if (remainder === 2) {
// aaaaaabb
const x = array[length - 2];
// bbbbcccc
const y = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2);
result += characterSet[a] + characterSet[b] + characterSet[c] + '=';
}
return result;
}

View file

@ -1,27 +0,0 @@
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// Polyfill for Chrome < 74
if (!TextEncoder.prototype.encodeInto) {
TextEncoder.prototype.encodeInto = function (source: string, destination: Uint8Array) {
const array = this.encode(source);
destination.set(array);
return { read: source.length, written: array.length };
}
}
export function decode(buffer: ArrayBuffer): string {
return textDecoder.decode(buffer);
}
export function encode(string: string): ArrayBuffer {
return textEncoder.encode(string);
}
export function encodeInto(string: string, buffer: Uint8Array): void {
textEncoder.encodeInto(string, buffer);
}
export function stringToArrayBuffer(input: string): ArrayBuffer {
return new Uint8Array(Array.from(input, c => c.charCodeAt(0))).buffer;
}

View file

@ -1,2 +0,0 @@
export * from './webadb';
export * from './transportation';

View file

@ -1,79 +0,0 @@
import { decode, encode, encodeInto } from './decode';
import { WebAdbTransportation } from './transportation';
export class AdbPacket {
public static parse(buffer: ArrayBuffer): AdbPacket {
const command = decode(buffer.slice(0, 4));
const view = new DataView(buffer);
const arg0 = view.getUint32(4, true);
const arg1 = view.getUint32(8, true);
const payloadLength = view.getUint32(12, true);
const packet = new AdbPacket(command, arg0, arg1, undefined);
packet._payloadLength = payloadLength;
return packet;
}
public command: string;
public arg0: number;
public arg1: number;
private _payloadLength!: number;
public get payloadLength(): number { return this._payloadLength; }
private _payload: ArrayBuffer | undefined;
public get payload(): ArrayBuffer | undefined { return this._payload; }
public set payload(value: ArrayBuffer | undefined) {
if (value !== undefined) {
this._payloadLength = value.byteLength;
this._payloadString = decode(value);
this._payload = value;
} else {
this._payloadLength = 0;
this._payloadString = undefined;
this._payload = undefined;
}
}
private _payloadString: string | undefined;
public get payloadString(): string | undefined { return this._payloadString; }
public constructor(command: string, arg0: number, arg1: number, payload?: string | ArrayBuffer) {
if (command.length !== 4) {
throw new TypeError('length of command must be 4');
}
this.command = command;
this.arg0 = arg0;
this.arg1 = arg1;
if (typeof payload === "string") {
this.payload = encode(payload);
} else {
this.payload = payload;
}
}
public async writeTo(transportation: WebAdbTransportation): Promise<void> {
const buffer = new ArrayBuffer(24);
const array = new Uint8Array(buffer);
const view = new DataView(buffer);
encodeInto(this.command, array);
view.setUint32(4, this.arg0, true);
view.setUint32(8, this.arg1, true);
view.setUint32(12, this.payloadLength, true);
view.setUint32(16, /* checksum */ 0, true);
view.setUint32(20, /* magic */ view.getUint32(0, true) ^ 0xFFFFFFFF, true);
await transportation.write(buffer);
if (this.payload) {
await transportation.write(this.payload);
}
}
}

View file

@ -5,7 +5,6 @@
"target": "ES2016", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "target": "ES2016", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ // /* Specify library files to be included in the compilation. */ "lib": [ // /* Specify library files to be included in the compilation. */
"DOM",
"ESNext" "ESNext"
], ],
// "allowJs": true, /* Allow javascript files to be compiled. */ // "allowJs": true, /* Allow javascript files to be compiled. */