@u4/adbkit
Version:
A Typescript client for the Android Debug Bridge.
287 lines • 9.62 kB
JavaScript
import { Buffer } from 'node:buffer';
import Protocol from './protocol.js';
import { AdbFailError, AdbPrematureEOFError, AdbUnexpectedDataError } from './errors.js';
import { Utils } from '../index.js';
/**
* helper to read in Duplex stream
*/
export default class Parser {
constructor(stream) {
this.stream = stream;
this.ended = false;
this.lastMessage = '';
// empty
}
/**
* read and drop all remaining data in stream
* @returns
*/
async end() {
if (this.ended) {
return true;
}
const stream = this.stream;
let tryRead;
let errorListener;
let endListener;
return new Promise((resolve, reject) => {
tryRead = () => {
while (stream.read()) {
// read and drop
}
};
errorListener = (err) => reject(err);
endListener = () => {
this.ended = true;
return resolve(true);
};
stream.on('readable', tryRead);
stream.on('error', errorListener);
stream.on('end', endListener);
stream.read(0);
stream.end();
}).finally(() => {
stream.removeListener('readable', tryRead);
stream.removeListener('error', errorListener);
stream.removeListener('end', endListener);
});
}
/**
* @returns the internal Duplex
*/
raw() {
return this.stream;
}
async readAll() {
let all = Buffer.alloc(0);
const stream = this.stream;
let tryRead;
let errorListener;
let endListener;
return new Promise((resolve, reject) => {
tryRead = () => {
let chunk;
while ((chunk = stream.read())) {
all = Utils.concatBuffer([all, chunk]);
}
if (this.ended) {
resolve(all);
}
};
errorListener = (err) => {
reject(err);
};
endListener = () => {
this.ended = true;
resolve(all);
};
stream.on('readable', tryRead);
stream.on('error', errorListener);
stream.on('end', endListener);
tryRead();
}).finally(() => {
stream.removeListener('readable', tryRead);
stream.removeListener('error', errorListener);
stream.removeListener('end', endListener);
});
}
async readAscii(howMany) {
const chunk = await this.readBytes(howMany);
return chunk.toString('ascii');
}
/**
* should read code or failed
* the correct fail exception will be thow in case of error.
* @param codes the expected 4 char code to read
*/
async readCode(...codes) {
const reply = await this.readAscii(4);
for (const code of codes)
if (code === reply)
return code;
if (reply === 'FAIL')
throw await this.readError();
throw this.unexpected(reply, `${codes.join(', ')} or FAIL`);
}
async readBytes(howMany) {
let tryRead;
let errorListener;
let endListener;
return new Promise((resolve, reject) => {
tryRead = () => {
if (howMany) {
const chunk = this.stream.read(howMany);
if (chunk) {
// If the stream ends while still having unread bytes, the read call
// will ignore the limit and just return what it's got.
howMany -= chunk.length;
if (howMany === 0) {
return resolve(chunk);
}
}
if (this.ended) {
return reject(new AdbPrematureEOFError(howMany, this.lastMessage));
}
}
else {
return resolve(Buffer.alloc(0));
}
};
endListener = () => {
this.ended = true;
return reject(new AdbPrematureEOFError(howMany, this.lastMessage));
};
errorListener = (err) => {
reject(err);
};
this.stream.on('readable', tryRead);
this.stream.on('error', errorListener);
this.stream.on('end', endListener);
tryRead();
}).finally(() => {
this.stream.removeListener('readable', tryRead);
this.stream.removeListener('error', errorListener);
this.stream.removeListener('end', endListener);
});
}
/**
* pipe howMany bytes to targetStream
* @param howMany bytes to transfer
* @param targetStream destination stream
* @returns Promise that resolves when all bytes are transferred
*/
async readByteFlow(howMany, targetStream) {
let tryRead;
let errorListener;
let endListener;
const stream = this.stream;
// TODO add cancellable
// const controller = new AbortController();
const promise = new Promise((resolve, reject) => {
tryRead = () => {
if (howMany) {
let chunk;
// Read timeout
// stream = addAbortSignal(controller.signal, this.stream);
// const tm = setTimeout(() => controller.abort(), 10_000); // set a timeout
// Try to get the exact amount we need first. If unsuccessful, take
// whatever is available, which will be less than the needed amount.
while ((chunk = stream.read(howMany) || stream.read())) {
howMany -= chunk.length;
// TODO fix missing backpressuring handling
targetStream.write(chunk);
if (howMany === 0) {
// clearTimeout(tm)
return resolve();
}
}
if (this.ended) {
// TODO add cancellable
//clearTimeout(tm)
return reject(new AdbPrematureEOFError(howMany, this.lastMessage));
}
}
else {
// TODO add cancellable
// clearTimeout(tm)
return resolve();
}
};
endListener = () => {
this.ended = true;
return reject(new AdbPrematureEOFError(howMany, this.lastMessage));
};
errorListener = (err) => {
return reject(err);
};
stream.on('readable', tryRead);
stream.on('error', errorListener);
stream.on('end', endListener);
tryRead();
}).finally(() => {
stream.removeListener('readable', tryRead);
stream.removeListener('error', errorListener);
stream.removeListener('end', endListener);
});
// TODO add cancellable
// (promise as any).cancel = () => {
// controller.abort();
// }
return promise;
}
async readError() {
let error = 'unknown Error';
try {
error = await this.readValue('utf8');
}
catch (e) {
// keep localy generated error
if (e instanceof AdbFailError) {
return e;
}
else if (e instanceof AdbPrematureEOFError) {
return e;
}
else if (e instanceof AdbUnexpectedDataError) {
return e;
}
error += ` ${e}`;
}
return new AdbFailError(error, this.lastMessage);
}
async readValue(encoding) {
const value = await this.readAscii(4);
const length = Protocol.decodeLength(value);
const buf = await this.readBytes(length);
if (encoding) {
return buf.toString(encoding);
}
return buf;
}
/**
* read stream byte per byte until a delimiter is found
* @param code delimiter
* @returns buffer wothout delimiter
*/
async readUntil(code) {
let skipped = Buffer.alloc(0);
for (;;) {
const chunk = await this.readBytes(1);
if (chunk[0] === code) {
return skipped;
}
else {
skipped = Utils.concatBuffer([skipped, chunk]);
}
}
}
async searchLine(re) {
for (;;) {
const line = await this.readLine();
const match = re.exec(line);
if (match) {
return match;
}
}
}
/**
* read socket until \r\n
* @returns
*/
async readLine(encoding) {
// read until \n
const line = await this.readUntil(10);
// drop tailing \r if present
const len = line.length;
if (line[len - 1] === 13) {
return line.slice(0, -1).toString(encoding);
}
else {
return line.toString(encoding);
}
}
unexpected(data, expected) {
return new AdbUnexpectedDataError(data, expected, this.lastMessage);
}
}
//# sourceMappingURL=parser.js.map