appium-adb
Version:
Android Debug Bridge interface
722 lines • 30.8 kB
JavaScript
;
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_1 = require("../helpers");
const teen_process_1 = require("teen_process");
const logger_1 = require("../logger");
const node_path_1 = __importDefault(require("node:path"));
const lodash_1 = __importDefault(require("lodash"));
const support_1 = require("@appium/support");
const semver = __importStar(require("semver"));
const node_os_1 = __importDefault(require("node: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.
*
* @param pkg - The name of the package to be uninstalled.
* @param options - The set of uninstall options.
* @returns True if the package was found on the device and
* successfully uninstalled.
*/
async function uninstallApk(pkg, options = {}) {
logger_1.log.debug(`Uninstalling ${pkg}`);
if (!options.skipInstallCheck && !(await this.isAppInstalled(pkg))) {
logger_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) {
const err = e;
throw new Error(`Unable to uninstall APK. Original error: ${err.message}`);
}
logger_1.log.debug(`'adb ${cmd.join(' ')}' command output: ${stdout}`);
if (stdout.includes('Success')) {
logger_1.log.info(`${pkg} was successfully uninstalled`);
return true;
}
logger_1.log.info(`${pkg} was not uninstalled`);
return false;
}
/**
* Install the package after it was pushed to the device under test.
*
* @param apkPathOnDevice - The full path to the package on the device file system.
* @param opts - Additional exec options.
* @throws If there was a failure during application install.
*/
async function installFromDevicePath(apkPathOnDevice, opts = {}) {
const stdout = 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.
*
* @param apkPath - Full path to the apk on the local FS
* @param options - Caching options
* @returns Full path to the cached apk on the remote file system
* @throws if there was a failure while caching the app
*/
async function cacheApk(apkPath, options = {}) {
const appHash = await support_1.fs.hash(apkPath);
const remotePath = node_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_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) {
const err = e;
logger_1.log.debug(`Got an error '${err.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_1.log.debug(`The count of applications in the cache: ${remoteCachedFiles.length}`);
const toHash = (remotePath) => node_path_1.default.posix.parse(remotePath).name;
// Push the apk to the remote cache if needed
if (remoteCachedFiles.some((x) => toHash(x) === appHash)) {
logger_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_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_1.log.info(`The upload of '${node_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: this.remoteAppsCacheLimit,
});
}
// Cleanup the invalid entries from the cache
lodash_1.default.difference([...this.remoteAppsCache.keys()], remoteCachedFiles.map(toHash)).forEach((hash) => 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) => node_path_1.default.posix.join(exports.REMOTE_CACHE_ROOT, x))
.filter((x) => !this.remoteAppsCache.has(toHash(x)))
.slice(this.remoteAppsCacheLimit - [...this.remoteAppsCache.keys()].length);
if (!lodash_1.default.isEmpty(entriesToCleanup)) {
try {
await this.shell(['rm', '-f', ...entriesToCleanup]);
logger_1.log.debug(`Deleted ${entriesToCleanup.length} expired application cache entries`);
}
catch (e) {
const err = e;
logger_1.log.warn(`Cannot delete ${entriesToCleanup.length} expired application cache entries. ` +
`Original error: ${err.message}`);
}
}
return remotePath;
}
/**
* Install the package from the local file system.
*
* @param appPath - The full path to the local package.
* @param options - The set of installation options.
* @throws If an unexpected error happens during install.
*/
async function install(appPath, options = {}) {
if (appPath.endsWith(helpers_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_1.DEFAULT_ADB_EXEC_TIMEOUT ? helpers_1.APK_INSTALL_TIMEOUT : this.adbExecTimeout,
timeoutCapName: 'androidInstallTimeout',
});
const installArgs = (0, helpers_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];
try {
const timer = new support_1.timing.Timer().start();
const output = await this.adbExec(installCmd, installOpts);
logger_1.log.info(`The installation of '${node_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_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_1.log.warn(msg);
throw new Error(`${output}\n${msg}`);
}
throw new Error(output);
}
}
catch (err) {
const error = err;
// on some systems this will throw an error if the app already
// exists
if (!error.message.includes('INSTALL_FAILED_ALREADY_EXISTS')) {
throw error;
}
logger_1.log.debug(`Application '${appPath}' already installed. Continuing.`);
}
}
/**
* Retrieves the current installation state of the particular application
*
* @param appPath - Full path to the application
* @param pkg - Package identifier. If omitted then the script will
* try to extract it on its own
* @returns One of `APP_INSTALL_STATE` constants
*/
async function getApplicationInstallState(appPath, pkg = null) {
let apkInfo = null;
if (!pkg) {
apkInfo = (await this.getApkInfo(appPath));
pkg = apkInfo?.name;
}
if (!pkg) {
logger_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_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 values 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_1.log.warn(`Cannot read version codes of '${appPath}' and/or '${pkg}'`);
if (!lodash_1.default.isString(apkVersionName) || !lodash_1.default.isString(pkgVersionName)) {
logger_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 (pkgVersionCode > apkVersionCode) {
logger_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_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_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_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_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.
*
* @param appPath - The full path to the local package.
* @param pkg - The name of the installed package. The method will
* perform faster if it is set.
* @param options - Set of install options.
* @throws If an unexpected error happens during install.
*/
async function installOrUpgrade(appPath, pkg = null, options = {}) {
if (!pkg) {
const apkInfo = await this.getApkInfo(appPath);
if ('name' in apkInfo) {
pkg = apkInfo.name;
}
else {
logger_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(pkg, { skipInstallCheck: true }))) {
throw new Error(`'${pkg}' package cannot be uninstalled`);
}
wasUninstalled = true;
};
switch (appState) {
case this.APP_INSTALL_STATE.NOT_INSTALLED:
logger_1.log.debug(`Installing '${appPath}'`);
await this.install(appPath, { ...options, replace: false });
return {
appState,
wasUninstalled,
};
case this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED:
if (enforceCurrentBuild) {
logger_1.log.info(`Downgrading '${pkg}' as requested`);
await uninstallPackage();
break;
}
logger_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_1.log.debug(`There is no need to install/upgrade '${appPath}'`);
return {
appState,
wasUninstalled,
};
case this.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED:
logger_1.log.debug(`Executing upgrade of '${appPath}'`);
break;
default:
logger_1.log.debug(`The current install state of '${appPath}' is unknown. Installing anyway`);
break;
}
try {
await this.install(appPath, { ...options, replace: true });
}
catch (err) {
const error = err;
logger_1.log.warn(`Cannot install/upgrade '${pkg}' because of '${error.message}'. Trying full reinstall`);
await uninstallPackage();
await this.install(appPath, { ...options, replace: false });
}
return {
appState,
wasUninstalled,
};
}
/**
* Extract string resources from the given package on local file system.
*
* @param appPath - The full path to the .apk(s) package.
* @param language - The name of the language to extract the resources for.
* The default language is used if this equals to `null`
* @param outRoot - 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.
*/
async function extractStringsFromApk(appPath, language = null, outRoot = null) {
logger_1.log.debug(`Extracting strings from for language: ${language || 'default'}`);
const originalAppPath = appPath;
if (appPath.endsWith(helpers_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)(this.binaries.aapt, [
'd',
'configurations',
appPath,
]);
return lodash_1.default.uniq(stdout.split(node_os_1.default.EOL));
}, language, '(default)');
const { stdout } = await (0, teen_process_1.exec)(this.binaries.aapt, [
'd',
'--values',
'resources',
appPath,
]);
apkStrings = parseAaptStrings(stdout, configMarker);
}
catch (e) {
const err = e;
logger_1.log.debug('Cannot extract resources using aapt. Trying aapt2. ' +
`Original error: ${err.stderr || err.message}`);
await this.initAapt2();
configMarker = await formatConfigMarker(async () => {
const { stdout } = await (0, teen_process_1.exec)(this.binaries.aapt2, [
'd',
'configurations',
appPath,
]);
return lodash_1.default.uniq(stdout.split(node_os_1.default.EOL));
}, language, '');
try {
const { stdout } = await (0, teen_process_1.exec)(this.binaries.aapt2, [
'd',
'resources',
appPath,
]);
apkStrings = parseAapt2Strings(stdout, configMarker);
}
catch (e) {
const error = e;
throw new Error(`Cannot extract resources from '${originalAppPath}'. ` + `Original error: ${error.message}`);
}
}
if (lodash_1.default.isEmpty(apkStrings)) {
logger_1.log.warn(`No strings have been found in '${originalAppPath}' resources ` +
`for '${configMarker || 'default'}' configuration`);
}
else {
logger_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 = node_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.
*
* @param appPath - The full path to existing .apk(s) package on the local
* file system.
* @returns 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_1.APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}
try {
const { name, versionCode, versionName } = await helpers_1.readPackageManifest.bind(this)(appPath);
return {
name,
versionCode,
versionName,
};
}
catch (e) {
const err = e;
logger_1.log.warn(`Error '${err.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 configsGetter The function whose result is a list
* of apk configs
* @param desiredMarker The desired config marker value
* @param defaultMarker The default config marker value
* @returns 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_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_1.log.debug(`Resource configuration name '${configMarker}' is unknown. ` +
`Replacing it with '${defaultMarker}'`);
configMarker = defaultMarker;
}
else {
logger_1.log.debug(`Selected configuration: '${configMarker}'`);
}
return configMarker;
}
/**
* Parses apk strings from aapt2 tool output
*
* @param rawOutput The actual tool output
* @param configMarker The config marker. Usually
* a language abbreviation or an empty string for the default one
* @returns 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(node_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] = [
...(Array.isArray(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 rawOutput The actual tool output
* @param configMarker The config marker. Usually
* a language abbreviation or `(default)`
* @returns 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(node_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] = [
...(Array.isArray(apkStrings[currentResourceId]) ? apkStrings[currentResourceId] : []),
normalizeStringMatch(match[0]),
];
}
continue;
}
}
return apkStrings;
}
// #endregion
//# sourceMappingURL=apk-utils.js.map