@u4/adbkit
Version:
A Typescript client for the Android Debug Bridge.
351 lines • 15.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdbSyncStatErrorCode = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const events_1 = __importDefault(require("events"));
const protocol_1 = __importDefault(require("./protocol"));
const stats_1 = __importDefault(require("./sync/stats"));
const entry_1 = __importDefault(require("./sync/entry"));
const pushtransfer_1 = __importDefault(require("./sync/pushtransfer"));
const pulltransfer_1 = __importDefault(require("./sync/pulltransfer"));
const stats64_1 = __importDefault(require("./sync/stats64"));
const entry64_1 = __importDefault(require("./sync/entry64"));
const utils_1 = __importDefault(require("./utils"));
const TEMP_PATH = '/data/local/tmp';
const DEFAULT_CHMOD = 0o644;
const DATA_MAX_LENGTH = 65536;
const debug = utils_1.default.debug('adb:sync');
const b1m = BigInt(1000000); //1000000n in es next
/**
* error code from STA2
*/
var AdbSyncStatErrorCode;
(function (AdbSyncStatErrorCode) {
AdbSyncStatErrorCode[AdbSyncStatErrorCode["SUCCESS"] = 0] = "SUCCESS";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EACCES"] = 13] = "EACCES";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EEXIST"] = 17] = "EEXIST";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EFAULT"] = 14] = "EFAULT";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EFBIG"] = 27] = "EFBIG";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EINTR"] = 4] = "EINTR";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EINVAL"] = 22] = "EINVAL";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EIO"] = 5] = "EIO";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EISDIR"] = 21] = "EISDIR";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ELOOP"] = 40] = "ELOOP";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EMFILE"] = 24] = "EMFILE";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ENAMETOOLONG"] = 36] = "ENAMETOOLONG";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ENFILE"] = 23] = "ENFILE";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ENOENT"] = 2] = "ENOENT";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ENOMEM"] = 12] = "ENOMEM";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ENOSPC"] = 28] = "ENOSPC";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ENOTDIR"] = 20] = "ENOTDIR";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EOVERFLOW"] = 75] = "EOVERFLOW";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EPERM"] = 1] = "EPERM";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["EROFS"] = 30] = "EROFS";
AdbSyncStatErrorCode[AdbSyncStatErrorCode["ETXTBSY"] = 26] = "ETXTBSY";
})(AdbSyncStatErrorCode || (exports.AdbSyncStatErrorCode = AdbSyncStatErrorCode = {}));
const STREAM_READ_TIMEOUT = 10000;
class Sync extends events_1.default {
/**
* get a temp file path
* @param path filename
* @returns full path on android devices
*/
static temp(path) {
return `${TEMP_PATH}/${path_1.default.basename(path)}`;
}
constructor(connection) {
super();
this.connection = connection;
this.on = (event, listener) => super.on(event, listener);
this.off = (event, listener) => super.off(event, listener);
this.once = (event, listener) => super.once(event, listener);
this.emit = (event, ...args) => super.emit(event, ...args);
this.parser = this.connection.parser;
}
/**
* Retrieves information about the given path.
* @param path The path.
* @returns An [`fs.Stats`][node-fs-stats] instance. While the `stats.is*` methods are available, only the following properties are supported:
* * **mode** The raw mode.
* * **size** The file size.
* * **mtime** The time of last modification as a `Date`.
*/
async stat(path) {
await this.sendCommandWithArg(protocol_1.default.STAT, path);
await this.parser.readCode(protocol_1.default.STAT);
const stat = await this.parser.readBytes(12);
const mode = stat.readUInt32LE(0);
const size = stat.readUInt32LE(4);
const mtime = stat.readUInt32LE(8);
if (mode === 0) {
return this.enoent(path);
}
else {
return new stats_1.default(mode, size, mtime);
}
}
async stat64(path) {
await this.sendCommandWithArg(protocol_1.default.STA2, path);
await this.parser.readCode(protocol_1.default.STA2);
const stat = await this.parser.readBytes(68); // IQQIIIIQqqq https://daeken.svbtle.com/arbitrary-file-write-by-adb-pull
const error = stat.readUInt32LE(0);
const dev = stat.readBigUint64LE(4);
const ino = stat.readBigUint64LE(12);
const mode = stat.readUInt32LE(20);
const nlink = BigInt(stat.readUInt32LE(24));
const uid = BigInt(stat.readUInt32LE(28));
const gid = BigInt(stat.readUInt32LE(32));
const size = stat.readBigUint64LE(36);
const atime = stat.readBigUint64LE(44) * b1m;
const mtime = stat.readBigUint64LE(52) * b1m;
const ctime = stat.readBigUint64LE(60) * b1m;
if (mode === 0) {
return this.enoent(path);
}
else {
return new stats64_1.default(error, dev, ino, BigInt(mode), nlink, uid, gid, size, atime, mtime, ctime);
}
}
/**
* Retrieves a list of directory entries (e.g. files) in the given path, not including the `.` and `..` entries, just like [`fs.readdir`][node-fs]. If given a non-directory path, no entries are returned.
*
* @param path The path.
* @returns An `Array` of [`fs.Stats`][node-fs-stats]-compatible instances. While the `stats.is*` methods are available, only the following properties are supported (in addition to the `name` field which contains the filename):
* * **name** The filename.
* * **mode** The raw mode.
* * **size** The file size.
* * **mtime** The time of last modification as a `Date`.
*/
async readdir(path) {
const files = [];
await this.sendCommandWithArg(protocol_1.default.LIST, path);
for (;;) {
const reply = await this.parser.readCode(protocol_1.default.DENT, protocol_1.default.DONE);
if (reply === protocol_1.default.DONE) {
await this.parser.readBytes(16);
return files;
}
const stat = await this.parser.readBytes(16);
const mode = stat.readUInt32LE(0);
const size = stat.readUInt32LE(4);
const mtime = stat.readUInt32LE(8);
const namelen = stat.readUInt32LE(12);
const name = await this.parser.readBytes(namelen);
const nameString = name.toString();
// Skip '.' and '..' to match Node's fs.readdir().
if (!(nameString === '.' || nameString === '..')) {
files.push(new entry_1.default(nameString, mode, size, mtime));
}
}
}
async readdir64(path) {
const files = [];
await this.sendCommandWithArg(protocol_1.default.LIS2, path);
for (;;) {
const reply = await this.parser.readCode(protocol_1.default.DNT2, protocol_1.default.DONE);
if (reply === protocol_1.default.DONE) {
await this.parser.readBytes(16);
return files;
}
const stat = await this.parser.readBytes(72); // IQQIIIIQqqqI // https://daeken.svbtle.com/arbitrary-file-write-by-adb-pull
const error = stat.readUInt32LE(0);
const dev = stat.readBigUint64LE(4);
const ino = stat.readBigUint64LE(12);
const mode = stat.readUInt32LE(20);
const nlink = BigInt(stat.readUInt32LE(24));
const uid = BigInt(stat.readUInt32LE(28));
const gid = BigInt(stat.readUInt32LE(32));
const size = stat.readBigUint64LE(36);
const atime = stat.readBigUint64LE(44) * b1m;
const mtime = stat.readBigUint64LE(52) * b1m;
const ctime = stat.readBigUint64LE(60) * b1m;
const namelen = stat.readUInt32LE(68); // I
const name = await this.parser.readBytes(namelen);
const nameString = name.toString();
// Skip '.' and '..' to match Node's fs.readdir().
if (!(nameString === '.' || nameString === '..')) {
files.push(new entry64_1.default(nameString, error, dev, ino, BigInt(mode), nlink, uid, gid, size, atime, mtime, ctime));
}
}
}
/**
* Attempts to identify `contents` and calls the appropriate `push*` method for it.
*
* @param contents When `String`, treated as a local file path and forwarded to `sync.pushFile()`. Otherwise, treated as a [`Stream`][node-stream] and forwarded to `sync.pushStream()`.
* @param path The path to push to.
* @param mode Optional. The mode of the file. Defaults to `0644`.
* @returns A `PushTransfer` instance. See below for details.
*/
async push(contents, path, mode, streamName = 'stream') {
if (typeof contents === 'string') {
return this.pushFile(contents, path, mode);
}
else {
return this.pushStream(contents, path, mode, streamName);
}
}
/**
* Pushes a local file to the given path. Note that the path must be writable by the ADB user (usually `shell`). When in doubt, use `'/data/local/tmp'` with an appropriate filename.
*
* @param file The local file path.
* @param path See `sync.push()` for details.
* @param mode See `sync.push()` for details.
* @returns See `sync.push()` for details.
*/
async pushFile(file, path, mode = DEFAULT_CHMOD) {
// mode || (mode = DEFAULT_CHMOD);
try {
const stats = await fs_1.default.promises.stat(file);
if (stats.isDirectory())
throw Error(`can not push directory "${file}" only files are supported for now.`);
}
catch (e) {
throw Error(`can not read file "${file}" Err: ${JSON.stringify(e)}`);
}
const stream = fs_1.default.createReadStream(file);
return this.pushStream(stream, path, mode, file);
}
/**
* Pushes a [`Stream`][node-stream] to the given path. Note that the path must be writable by the ADB user (usually `shell`). When in doubt, use `'/data/local/tmp'` with an appropriate filename.
*
* @param stream The readable stream.
* @param path See `sync.push()` for details.
* @param mode See `sync.push()` for details.
* @returns See `sync.push()` for details.
*/
async pushStream(stream, path, mode = DEFAULT_CHMOD, streamName = 'stream') {
mode |= stats_1.default.S_IFREG;
await this.sendCommandWithArg(protocol_1.default.SEND, `${path},${mode}`);
return this._writeData(stream, Math.floor(Date.now() / 1000), streamName);
}
/**
* Pulls a file from the device as a `PullTransfer` [`Stream`][node-stream].
* @param path The path to pull from.
* @returns A `PullTransfer` instance. See below for details.
*/
async pull(path) {
await this.sendCommandWithArg(protocol_1.default.RECV, `${path}`);
return this.readData();
}
/**
* Closes the Sync connection, allowing Node to quit (assuming nothing else is keeping it alive, of course).
* @returns Returns: The sync instance.
*/
end() {
this.connection.end();
return this;
}
/**
* A simple helper method for creating appropriate temporary filenames for pushing files. This is essentially the same as taking the basename of the file and appending it to `'/data/local/tmp/'`.
*
* @param path The path of the file.
* @returns An appropriate temporary file path.
*/
tempFile(path) {
return Sync.temp(path);
}
async _writeData(stream, timeStamp, streamName) {
const transfer = new pushtransfer_1.default();
stream.once('error', (err) => {
throw new Error(`Source Error: ${err.message} while transfering ${streamName}`);
});
this.connection.once('error', (err) => {
stream.destroy(err);
this.connection.end();
throw new Error(`Target Error: ${err.message} while transfering ${streamName}`);
});
for (let i = 0;; i++) {
if (stream.closed)
break;
const readable = await utils_1.default.waitforReadable(stream, STREAM_READ_TIMEOUT);
if (!readable)
break;
let chunk;
// eslint-disable-next-line no-cond-assign
while (chunk = (stream.read(DATA_MAX_LENGTH) || stream.read())) {
await this.sendCommandWithLength(protocol_1.default.DATA, chunk.length);
transfer.push(chunk.length);
await this.connection.write(chunk);
transfer.pop();
}
}
await this.sendCommandWithLength(protocol_1.default.DONE, timeStamp);
try {
await this.parser.readCode(protocol_1.default.OKAY);
}
catch (err) {
transfer.emit('error', err);
}
finally {
transfer.end();
}
return transfer;
}
readData() {
const transfer = new pulltransfer_1.default();
const readAll = async () => {
for (;;) {
const reply = await this.parser.readCode(protocol_1.default.DATA, protocol_1.default.DONE);
if (reply === protocol_1.default.DONE) {
await this.parser.readBytes(4);
return true;
}
const lengthData = await this.parser.readBytes(4);
const length = lengthData.readUInt32LE(0);
await this.parser.readByteFlow(length, transfer);
}
};
readAll().catch(err => {
transfer.emit('error', err);
}).finally(() => {
return transfer.end();
});
return transfer;
}
/**
*
* @param cmd
* @param length
* @returns byte write count
*/
sendCommandWithLength(cmd, length) {
if (cmd !== protocol_1.default.DATA) {
debug(cmd);
}
const payload = Buffer.allocUnsafe(cmd.length + 4);
payload.write(cmd, 0, cmd.length);
payload.writeUInt32LE(length, cmd.length);
this.parser.lastMessage = `${cmd} ${length}`;
return this.connection.write(payload);
}
/**
*
* @param cmd
* @param arg
* @returns byte write count
*/
sendCommandWithArg(cmd, arg) {
this.parser.lastMessage = `${cmd} ${arg}`;
debug(this.parser.lastMessage);
const arglen = Buffer.byteLength(arg, 'utf-8');
const payload = Buffer.allocUnsafe(cmd.length + 4 + arglen);
payload.write(cmd, 0, cmd.length);
payload.writeUInt32LE(arglen, cmd.length);
payload.write(arg, cmd.length + 4);
return this.connection.write(payload);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
enoent(path) {
const err = new Error(`ENOENT, no such file or directory '${path}'`);
err.errno = 34;
err.code = 'ENOENT';
err.path = path;
return Promise.reject(err);
}
}
exports.default = Sync;
//# sourceMappingURL=sync.js.map