UNPKG

@u4/adbkit

Version:

A Typescript client for the Android Debug Bridge.

351 lines 15.6 kB
"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