UNPKG

appium-adb

Version:

Android Debug Bridge interface

765 lines 34.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.REMOTE_CACHE_ROOT = void 0; exports.uninstallApk = uninstallApk; exports.installFromDevicePath = installFromDevicePath; exports.cacheApk = cacheApk; exports.install = install; exports.getApplicationInstallState = getApplicationInstallState; exports.installOrUpgrade = installOrUpgrade; exports.extractStringsFromApk = extractStringsFromApk; exports.getApkInfo = getApkInfo; exports.parseAapt2Strings = parseAapt2Strings; exports.parseAaptStrings = parseAaptStrings; const helpers_js_1 = require("../helpers.js"); const teen_process_1 = require("teen_process"); const logger_js_1 = require("../logger.js"); const path_1 = __importDefault(require("path")); const lodash_1 = __importDefault(require("lodash")); const support_1 = require("@appium/support"); const semver = __importStar(require("semver")); const os_1 = __importDefault(require("os")); const lru_cache_1 = require("lru-cache"); exports.REMOTE_CACHE_ROOT = '/data/local/tmp/appium_cache'; /** * Uninstall the given package from the device under test. * * @this {import('../adb.js').ADB} * @param {string} pkg - The name of the package to be uninstalled. * @param {import('./types').UninstallOptions} [options={}] - The set of uninstall options. * @return {Promise<boolean>} True if the package was found on the device and * successfully uninstalled. */ async function uninstallApk(pkg, options = {}) { logger_js_1.log.debug(`Uninstalling ${pkg}`); if (!options.skipInstallCheck && !await this.isAppInstalled(pkg)) { logger_js_1.log.info(`${pkg} was not uninstalled, because it was not present on the device`); return false; } const cmd = ['uninstall']; if (options.keepData) { cmd.push('-k'); } cmd.push(pkg); let stdout; try { await this.forceStop(pkg); stdout = (await this.adbExec(cmd, { timeout: options.timeout })).trim(); } catch (e) { throw new Error(`Unable to uninstall APK. Original error: ${e.message}`); } logger_js_1.log.debug(`'adb ${cmd.join(' ')}' command output: ${stdout}`); if (stdout.includes('Success')) { logger_js_1.log.info(`${pkg} was successfully uninstalled`); return true; } logger_js_1.log.info(`${pkg} was not uninstalled`); return false; } /** * Install the package after it was pushed to the device under test. * * @this {import('../adb.js').ADB} * @param {string} apkPathOnDevice - The full path to the package on the device file system. * @param {import('./types').ShellExecOptions} [opts={}] Additional exec options. * @throws {error} If there was a failure during application install. */ async function installFromDevicePath(apkPathOnDevice, opts = {}) { const stdout = /** @type {string} */ (await this.shell(['pm', 'install', '-r', apkPathOnDevice], opts)); if (stdout.includes('Failure')) { throw new Error(`Remote install failed: ${stdout}`); } } /** * Caches the given APK at a remote location to speed up further APK deployments. * * @this {import('../adb.js').ADB} * @param {string} apkPath - Full path to the apk on the local FS * @param {import('./types').CachingOptions} [options={}] - Caching options * @returns {Promise<string>} - Full path to the cached apk on the remote file system * @throws {Error} if there was a failure while caching the app */ async function cacheApk(apkPath, options = {}) { const appHash = await support_1.fs.hash(apkPath); const remotePath = path_1.default.posix.join(exports.REMOTE_CACHE_ROOT, `${appHash}.apk`); const remoteCachedFiles = []; // Get current contents of the remote cache or create it for the first time try { const errorMarker = '_ERROR_'; let lsOutput = null; if (this._areExtendedLsOptionsSupported === true || !lodash_1.default.isBoolean(this._areExtendedLsOptionsSupported)) { lsOutput = await this.shell([`ls -t -1 ${exports.REMOTE_CACHE_ROOT} 2>&1 || echo ${errorMarker}`]); } if (!lodash_1.default.isString(lsOutput) || (lsOutput.includes(errorMarker) && !lsOutput.includes(exports.REMOTE_CACHE_ROOT))) { if (!lodash_1.default.isBoolean(this._areExtendedLsOptionsSupported)) { logger_js_1.log.debug('The current Android API does not support extended ls options. ' + 'Defaulting to no-options call'); } lsOutput = await this.shell([`ls ${exports.REMOTE_CACHE_ROOT} 2>&1 || echo ${errorMarker}`]); this._areExtendedLsOptionsSupported = false; } else { this._areExtendedLsOptionsSupported = true; } if (lsOutput.includes(errorMarker)) { throw new Error(lsOutput.substring(0, lsOutput.indexOf(errorMarker))); } remoteCachedFiles.push(...(lsOutput.split('\n') .map((x) => x.trim()) .filter(Boolean))); } catch (e) { logger_js_1.log.debug(`Got an error '${e.message.trim()}' while getting the list of files in the cache. ` + `Assuming the cache does not exist yet`); await this.shell(['mkdir', '-p', exports.REMOTE_CACHE_ROOT]); } logger_js_1.log.debug(`The count of applications in the cache: ${remoteCachedFiles.length}`); const toHash = (remotePath) => path_1.default.posix.parse(remotePath).name; // Push the apk to the remote cache if needed if (remoteCachedFiles.some((x) => toHash(x) === appHash)) { logger_js_1.log.info(`The application at '${apkPath}' is already cached to '${remotePath}'`); // Update the application timestamp asynchronously in order to bump its position // in the sorted ls output // eslint-disable-next-line promise/prefer-await-to-then this.shell(['touch', '-am', remotePath]).catch(() => { }); } else { logger_js_1.log.info(`Caching the application at '${apkPath}' to '${remotePath}'`); const timer = new support_1.timing.Timer().start(); await this.push(apkPath, remotePath, { timeout: options.timeout }); const { size } = await support_1.fs.stat(apkPath); logger_js_1.log.info(`The upload of '${path_1.default.basename(apkPath)}' (${support_1.util.toReadableSizeString(size)}) ` + `took ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } if (!this.remoteAppsCache) { this.remoteAppsCache = new lru_cache_1.LRUCache({ max: /** @type {number} */ (this.remoteAppsCacheLimit), }); } // Cleanup the invalid entries from the cache lodash_1.default.difference([...this.remoteAppsCache.keys()], remoteCachedFiles.map(toHash)) .forEach((hash) => ( /** @type {LRUCache} */(this.remoteAppsCache)).delete(hash)); // Bump the cache record for the recently cached item this.remoteAppsCache.set(appHash, remotePath); // If the remote cache exceeds this.remoteAppsCacheLimit, remove the least recently used entries const entriesToCleanup = remoteCachedFiles .map((x) => path_1.default.posix.join(exports.REMOTE_CACHE_ROOT, x)) .filter((x) => !( /** @type {LRUCache} */(this.remoteAppsCache)).has(toHash(x))) .slice(( /** @type {number} */(this.remoteAppsCacheLimit)) - [...this.remoteAppsCache.keys()].length); if (!lodash_1.default.isEmpty(entriesToCleanup)) { try { await this.shell(['rm', '-f', ...entriesToCleanup]); logger_js_1.log.debug(`Deleted ${entriesToCleanup.length} expired application cache entries`); } catch (e) { logger_js_1.log.warn(`Cannot delete ${entriesToCleanup.length} expired application cache entries. ` + `Original error: ${e.message}`); } } return remotePath; } /** * Install the package from the local file system. * * @this {import('../adb.js').ADB} * @param {string} appPath - The full path to the local package. * @param {import('./types').InstallOptions} [options={}] - The set of installation options. * @throws {Error} If an unexpected error happens during install. */ async function install(appPath, options = {}) { if (appPath.endsWith(helpers_js_1.APKS_EXTENSION)) { return await this.installApks(appPath, options); } options = lodash_1.default.cloneDeep(options); lodash_1.default.defaults(options, { replace: true, timeout: this.adbExecTimeout === helpers_js_1.DEFAULT_ADB_EXEC_TIMEOUT ? helpers_js_1.APK_INSTALL_TIMEOUT : this.adbExecTimeout, timeoutCapName: 'androidInstallTimeout', }); const installArgs = (0, helpers_js_1.buildInstallArgs)(await this.getApiLevel(), options); if (options.noIncremental && await this.isIncrementalInstallSupported()) { // Adb throws an error if it does not know about an arg, // which is the case here for older adb versions. installArgs.push('--no-incremental'); } const installOpts = { timeout: options.timeout, timeoutCapName: options.timeoutCapName, }; const installCmd = [ 'install', ...installArgs, appPath, ]; let performAppInstall = async () => await this.adbExec(installCmd, installOpts); // this.remoteAppsCacheLimit <= 0 means no caching should be applied let shouldCacheApp = ( /** @type {number} */(this.remoteAppsCacheLimit)) > 0; if (shouldCacheApp) { shouldCacheApp = !(await this.isStreamedInstallSupported()); if (!shouldCacheApp) { logger_js_1.log.info(`The application at '${appPath}' will not be cached, because the device under test has ` + `confirmed the support of streamed installs`); } } if (shouldCacheApp) { const clearCache = async () => { logger_js_1.log.info(`Clearing the cache at '${exports.REMOTE_CACHE_ROOT}'`); await this.shell(['rm', '-rf', `${exports.REMOTE_CACHE_ROOT}/*`]); }; const cacheApp = async () => await this.cacheApk(appPath, { timeout: options.timeout, }); try { const cachedAppPath = await cacheApp(); performAppInstall = async () => { const pmInstallCmdByRemotePath = (remotePath) => [ 'pm', 'install', ...installArgs, remotePath, ]; const output = await this.shell(pmInstallCmdByRemotePath(cachedAppPath), installOpts); // https://github.com/appium/appium/issues/13970 if (/\bINSTALL_FAILED_INSUFFICIENT_STORAGE\b/.test(output)) { logger_js_1.log.warn(`There was a failure while installing '${appPath}' ` + `because of the insufficient device storage space`); await clearCache(); logger_js_1.log.info(`Consider decreasing the maximum amount of cached apps ` + `(currently ${this.remoteAppsCacheLimit}) to avoid such issues in the future`); const newCachedAppPath = await cacheApp(); return await this.shell(pmInstallCmdByRemotePath(newCachedAppPath), installOpts); } return output; }; } catch (e) { logger_js_1.log.debug(e); logger_js_1.log.warn(`There was a failure while caching '${appPath}': ${e.message}`); logger_js_1.log.warn('Falling back to the default installation procedure'); await clearCache(); } } try { const timer = new support_1.timing.Timer().start(); const output = /** @type {string} */ (await performAppInstall()); logger_js_1.log.info(`The installation of '${path_1.default.basename(appPath)}' took ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); const truncatedOutput = (!lodash_1.default.isString(output) || output.length <= 300) ? output : `${output.substring(0, 150)}...${output.substring(output.length - 150)}`; logger_js_1.log.debug(`Install command stdout: ${truncatedOutput}`); if (/\[INSTALL[A-Z_]+FAILED[A-Z_]+\]/.test(output)) { if (this.isTestPackageOnlyError(output)) { const msg = `Set 'allowTestPackages' capability to true in order to allow test packages installation.`; logger_js_1.log.warn(msg); throw new Error(`${output}\n${msg}`); } throw new Error(output); } } catch (err) { // on some systems this will throw an error if the app already // exists if (!err.message.includes('INSTALL_FAILED_ALREADY_EXISTS')) { throw err; } logger_js_1.log.debug(`Application '${appPath}' already installed. Continuing.`); } } /** * Retrieves the current installation state of the particular application * * @this {import('../adb.js').ADB} * @param {string} appPath - Full path to the application * @param {string?} [pkg=null] - Package identifier. If omitted then the script will * try to extract it on its own * @returns {Promise<import('./types').InstallState>} One of `APP_INSTALL_STATE` constants */ async function getApplicationInstallState(appPath, pkg = null) { let apkInfo = null; if (!pkg) { apkInfo = await this.getApkInfo(appPath); // @ts-ignore We are ok if this prop does not exist pkg = apkInfo.name; } if (!pkg) { logger_js_1.log.warn(`Cannot read the package name of '${appPath}'`); return this.APP_INSTALL_STATE.UNKNOWN; } const { versionCode: pkgVersionCode, versionName: pkgVersionNameStr, isInstalled, } = await this.getPackageInfo(pkg); if (!isInstalled) { logger_js_1.log.debug(`App '${appPath}' is not installed`); return this.APP_INSTALL_STATE.NOT_INSTALLED; } const pkgVersionName = semver.valid(semver.coerce(pkgVersionNameStr)); if (!apkInfo) { apkInfo = await this.getApkInfo(appPath); } // @ts-ignore We validate the valus below const { versionCode: apkVersionCode, versionName: apkVersionNameStr } = apkInfo; const apkVersionName = semver.valid(semver.coerce(apkVersionNameStr)); if (!lodash_1.default.isInteger(apkVersionCode) || !lodash_1.default.isInteger(pkgVersionCode)) { logger_js_1.log.warn(`Cannot read version codes of '${appPath}' and/or '${pkg}'`); if (!lodash_1.default.isString(apkVersionName) || !lodash_1.default.isString(pkgVersionName)) { logger_js_1.log.warn(`Cannot read version names of '${appPath}' and/or '${pkg}'`); return this.APP_INSTALL_STATE.UNKNOWN; } } if (lodash_1.default.isInteger(apkVersionCode) && lodash_1.default.isInteger(pkgVersionCode)) { if (( /** @type {number} */(pkgVersionCode)) > apkVersionCode) { logger_js_1.log.debug(`The version code of the installed '${pkg}' is greater than the application version code (${pkgVersionCode} > ${apkVersionCode})`); return this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED; } // Version codes might not be maintained. Check version names. if (pkgVersionCode === apkVersionCode) { if (lodash_1.default.isString(apkVersionName) && lodash_1.default.isString(pkgVersionName) && semver.satisfies(pkgVersionName, `>=${apkVersionName}`)) { logger_js_1.log.debug(`The version name of the installed '${pkg}' is greater or equal to the application version name ('${pkgVersionName}' >= '${apkVersionName}')`); return semver.satisfies(pkgVersionName, `>${apkVersionName}`) ? this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED : this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED; } if (!lodash_1.default.isString(apkVersionName) || !lodash_1.default.isString(pkgVersionName)) { logger_js_1.log.debug(`The version name of the installed '${pkg}' is equal to application version name (${pkgVersionCode} === ${apkVersionCode})`); return this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED; } } } else if (lodash_1.default.isString(apkVersionName) && lodash_1.default.isString(pkgVersionName) && semver.satisfies(pkgVersionName, `>=${apkVersionName}`)) { logger_js_1.log.debug(`The version name of the installed '${pkg}' is greater or equal to the application version name ('${pkgVersionName}' >= '${apkVersionName}')`); return semver.satisfies(pkgVersionName, `>${apkVersionName}`) ? this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED : this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED; } logger_js_1.log.debug(`The installed '${pkg}' package is older than '${appPath}' (${pkgVersionCode} < ${apkVersionCode} or '${pkgVersionName}' < '${apkVersionName}')'`); return this.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED; } /** * Install the package from the local file system or upgrade it if an older * version of the same package is already installed. * * @this {import('../adb.js').ADB} * @param {string} appPath - The full path to the local package. * @param {string?} [pkg=null] - The name of the installed package. The method will * perform faster if it is set. * @param {import('./types').InstallOrUpgradeOptions} [options={}] - Set of install options. * @throws {Error} If an unexpected error happens during install. * @returns {Promise<import('./types').InstallOrUpgradeResult>} */ async function installOrUpgrade(appPath, pkg = null, options = {}) { if (!pkg) { const apkInfo = await this.getApkInfo(appPath); if ('name' in apkInfo) { pkg = apkInfo.name; } else { logger_js_1.log.warn(`Cannot determine the package name of '${appPath}'. ` + `Continuing with the install anyway`); } } const { enforceCurrentBuild, } = options; const appState = await this.getApplicationInstallState(appPath, pkg); let wasUninstalled = false; const uninstallPackage = async () => { if (!await this.uninstallApk(/** @type {string} */ (pkg), { skipInstallCheck: true })) { throw new Error(`'${pkg}' package cannot be uninstalled`); } wasUninstalled = true; }; switch (appState) { case this.APP_INSTALL_STATE.NOT_INSTALLED: logger_js_1.log.debug(`Installing '${appPath}'`); await this.install(appPath, Object.assign({}, options, { replace: false })); return { appState, wasUninstalled, }; case this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED: if (enforceCurrentBuild) { logger_js_1.log.info(`Downgrading '${pkg}' as requested`); await uninstallPackage(); break; } logger_js_1.log.debug(`There is no need to downgrade '${pkg}'`); return { appState, wasUninstalled, }; case this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED: if (enforceCurrentBuild) { break; } logger_js_1.log.debug(`There is no need to install/upgrade '${appPath}'`); return { appState, wasUninstalled, }; case this.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED: logger_js_1.log.debug(`Executing upgrade of '${appPath}'`); break; default: logger_js_1.log.debug(`The current install state of '${appPath}' is unknown. Installing anyway`); break; } try { await this.install(appPath, Object.assign({}, options, { replace: true })); } catch (err) { logger_js_1.log.warn(`Cannot install/upgrade '${pkg}' because of '${err.message}'. Trying full reinstall`); await uninstallPackage(); await this.install(appPath, Object.assign({}, options, { replace: false })); } return { appState, wasUninstalled, }; } /** * Extract string resources from the given package on local file system. * * @this {import('../adb.js').ADB} * @param {string} appPath - The full path to the .apk(s) package. * @param {string?} [language=null] - The name of the language to extract the resources for. * The default language is used if this equals to `null` * @param {string?} [outRoot=null] - The name of the destination folder on the local file system to * store the extracted file to. If not provided then the `localPath` property in the returned object * will be undefined. * @return {Promise<import('./types').ApkStrings>} */ async function extractStringsFromApk(appPath, language = null, outRoot = null) { logger_js_1.log.debug(`Extracting strings from for language: ${language || 'default'}`); const originalAppPath = appPath; if (appPath.endsWith(helpers_js_1.APKS_EXTENSION)) { appPath = await this.extractLanguageApk(appPath, language); } let apkStrings = {}; let configMarker; try { await this.initAapt(); configMarker = await formatConfigMarker(async () => { const { stdout } = await (0, teen_process_1.exec)(( /** @type {import('./types').StringRecord} */(this.binaries)).aapt, [ 'd', 'configurations', appPath, ]); return lodash_1.default.uniq(stdout.split(os_1.default.EOL)); }, language, '(default)'); const { stdout } = await (0, teen_process_1.exec)(( /** @type {import('./types').StringRecord} */(this.binaries)).aapt, [ 'd', '--values', 'resources', appPath, ]); apkStrings = parseAaptStrings(stdout, configMarker); } catch (e) { logger_js_1.log.debug('Cannot extract resources using aapt. Trying aapt2. ' + `Original error: ${e.stderr || e.message}`); await this.initAapt2(); configMarker = await formatConfigMarker(async () => { const { stdout } = await (0, teen_process_1.exec)(( /** @type {import('./types').StringRecord} */(this.binaries)).aapt2, [ 'd', 'configurations', appPath, ]); return lodash_1.default.uniq(stdout.split(os_1.default.EOL)); }, language, ''); try { const { stdout } = await (0, teen_process_1.exec)(( /** @type {import('./types').StringRecord} */(this.binaries)).aapt2, [ 'd', 'resources', appPath, ]); apkStrings = parseAapt2Strings(stdout, configMarker); } catch (e) { throw new Error(`Cannot extract resources from '${originalAppPath}'. ` + `Original error: ${e.message}`); } } if (lodash_1.default.isEmpty(apkStrings)) { logger_js_1.log.warn(`No strings have been found in '${originalAppPath}' resources ` + `for '${configMarker || 'default'}' configuration`); } else { logger_js_1.log.info(`Successfully extracted ${lodash_1.default.keys(apkStrings).length} strings from ` + `'${originalAppPath}' resources for '${configMarker || 'default'}' configuration`); } if (!outRoot) { return { apkStrings }; } const localPath = path_1.default.resolve(outRoot, 'strings.json'); await (0, support_1.mkdirp)(outRoot); await support_1.fs.writeFile(localPath, JSON.stringify(apkStrings, null, 2), 'utf-8'); return { apkStrings, localPath }; } /** * Get the package info from local apk file. * * @this {import('../adb.js').ADB} * @param {string} appPath - The full path to existing .apk(s) package on the local * file system. * @return {Promise<import('./types').AppInfo|{}>} The parsed application information. */ async function getApkInfo(appPath) { if (!await support_1.fs.exists(appPath)) { throw new Error(`The file at path ${appPath} does not exist or is not accessible`); } if (appPath.endsWith(helpers_js_1.APKS_EXTENSION)) { appPath = await this.extractBaseApk(appPath); } try { const { name, versionCode, versionName } = await helpers_js_1.readPackageManifest.bind(this)(appPath); return { name, versionCode, versionName, }; } catch (e) { logger_js_1.log.warn(`Error '${e.message}' while getting badging info`); } return {}; } // #region Private functions /** * Formats the config marker, which is then passed to parse.. methods * to make it compatible with resource formats generated by aapt(2) tool * * @param {Function} configsGetter The function whose result is a list * of apk configs * @param {string?} desiredMarker The desired config marker value * @param {string} defaultMarker The default config marker value * @return {Promise<string>} The formatted config marker */ async function formatConfigMarker(configsGetter, desiredMarker, defaultMarker) { let configMarker = desiredMarker || defaultMarker; if (configMarker.includes('-') && !configMarker.includes('-r')) { configMarker = configMarker.replace('-', '-r'); } const configs = await configsGetter(); logger_js_1.log.debug(`Resource configurations: ${JSON.stringify(configs)}`); // Assume the 'en' configuration is the default one if (configMarker.toLowerCase().startsWith('en') && !configs.some((x) => x.trim() === configMarker)) { logger_js_1.log.debug(`Resource configuration name '${configMarker}' is unknown. ` + `Replacing it with '${defaultMarker}'`); configMarker = defaultMarker; } else { logger_js_1.log.debug(`Selected configuration: '${configMarker}'`); } return configMarker; } /** * Parses apk strings from aapt2 tool output * * @param {string} rawOutput The actual tool output * @param {string} configMarker The config marker. Usually * a language abbreviation or an empty string for the default one * @returns {Object} Strings ids to values mapping. Plural * values are represented as arrays. If no config found for the * given marker then an empty mapping is returned. */ function parseAapt2Strings(rawOutput, configMarker) { const allLines = rawOutput.split(os_1.default.EOL); function extractContent(startIdx) { let idx = startIdx; const startCharPos = allLines[startIdx].indexOf('"'); if (startCharPos < 0) { return [null, idx]; } let result = ''; while (idx < allLines.length) { const terminationCharMatch = /"$/.exec(allLines[idx]); if (terminationCharMatch) { const terminationCharPos = terminationCharMatch.index; if (startIdx === idx) { return [ allLines[idx].substring(startCharPos + 1, terminationCharPos), idx ]; } return [ `${result}\\n${lodash_1.default.trimStart(allLines[idx].substring(0, terminationCharPos))}`, idx, ]; } if (idx > startIdx) { result += `\\n${lodash_1.default.trimStart(allLines[idx])}`; } else { result += allLines[idx].substring(startCharPos + 1); } ++idx; } return [result, idx]; } const apkStrings = {}; let currentResourceId = null; let isInPluralGroup = false; let isInCurrentConfig = false; let lineIndex = 0; while (lineIndex < allLines.length) { const trimmedLine = allLines[lineIndex].trim(); if (lodash_1.default.isEmpty(trimmedLine)) { ++lineIndex; continue; } if (['type', 'Package'].some((x) => trimmedLine.startsWith(x))) { currentResourceId = null; isInPluralGroup = false; isInCurrentConfig = false; ++lineIndex; continue; } if (trimmedLine.startsWith('resource')) { isInPluralGroup = false; currentResourceId = null; isInCurrentConfig = false; if (trimmedLine.includes('string/')) { const match = /string\/(\S+)/.exec(trimmedLine); if (match) { currentResourceId = match[1]; } } else if (trimmedLine.includes('plurals/')) { const match = /plurals\/(\S+)/.exec(trimmedLine); if (match) { currentResourceId = match[1]; isInPluralGroup = true; } } ++lineIndex; continue; } if (currentResourceId) { if (isInPluralGroup) { if (trimmedLine.startsWith('(')) { isInCurrentConfig = trimmedLine.startsWith(`(${configMarker})`); ++lineIndex; continue; } if (isInCurrentConfig) { const [content, idx] = extractContent(lineIndex); lineIndex = idx; if (lodash_1.default.isString(content)) { apkStrings[currentResourceId] = [ ...(apkStrings[currentResourceId] || []), content, ]; } } } else if (trimmedLine.startsWith(`(${configMarker})`)) { const [content, idx] = extractContent(lineIndex); lineIndex = idx; if (lodash_1.default.isString(content)) { apkStrings[currentResourceId] = content; } currentResourceId = null; } } ++lineIndex; } return apkStrings; } /** * Parses apk strings from aapt tool output * * @param {string} rawOutput The actual tool output * @param {string} configMarker The config marker. Usually * a language abbreviation or `(default)` * @returns {Object} Strings ids to values mapping. Plural * values are represented as arrays. If no config found for the * given marker then an empty mapping is returned. */ function parseAaptStrings(rawOutput, configMarker) { const normalizeStringMatch = function (s) { return s.replace(/"$/, '').replace(/^"/, '').replace(/\\"/g, '"'); }; const apkStrings = {}; let isInConfig = false; let currentResourceId = null; let isInPluralGroup = false; // The pattern matches any quoted content including escaped quotes const quotedStringPattern = /"[^"\\]*(?:\\.[^"\\]*)*"/; for (const line of rawOutput.split(os_1.default.EOL)) { const trimmedLine = line.trim(); if (lodash_1.default.isEmpty(trimmedLine)) { continue; } if (['config', 'type', 'spec', 'Package'].some((x) => trimmedLine.startsWith(x))) { isInConfig = trimmedLine.startsWith(`config ${configMarker}:`); currentResourceId = null; isInPluralGroup = false; continue; } if (!isInConfig) { continue; } if (trimmedLine.startsWith('resource')) { isInPluralGroup = false; currentResourceId = null; if (trimmedLine.includes(':string/')) { const match = /:string\/(\S+):/.exec(trimmedLine); if (match) { currentResourceId = match[1]; } } else if (trimmedLine.includes(':plurals/')) { const match = /:plurals\/(\S+):/.exec(trimmedLine); if (match) { currentResourceId = match[1]; isInPluralGroup = true; } } continue; } if (currentResourceId && trimmedLine.startsWith('(string')) { const match = quotedStringPattern.exec(trimmedLine); if (match) { apkStrings[currentResourceId] = normalizeStringMatch(match[0]); } currentResourceId = null; continue; } if (currentResourceId && isInPluralGroup && trimmedLine.includes(': (string')) { const match = quotedStringPattern.exec(trimmedLine); if (match) { apkStrings[currentResourceId] = [ ...(apkStrings[currentResourceId] || []), normalizeStringMatch(match[0]), ]; } continue; } } return apkStrings; } // #endregion //# sourceMappingURL=apk-utils.js.map