Skip to content

Commit

Permalink
chore: Only keep common functions in helpers (#792)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jan 25, 2025
1 parent be74664 commit 18e7919
Show file tree
Hide file tree
Showing 16 changed files with 903 additions and 886 deletions.
847 changes: 1 addition & 846 deletions lib/helpers.js

Large diffs are not rendered by default.

52 changes: 50 additions & 2 deletions lib/tools/android-manifest.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import _ from 'lodash';
import { exec } from 'teen_process';
import { log } from '../logger.js';
import {
getAndroidPlatformAndPath, unzipFile,
APKS_EXTENSION, readPackageManifest,
unzipFile,
APKS_EXTENSION,
readPackageManifest,
} from '../helpers.js';
import { fs, zip, tempDir, util } from '@appium/support';
import path from 'path';
Expand Down Expand Up @@ -208,3 +210,49 @@ export async function hasInternetPermissionFromManifest (appPath) {
const {usesPermissions} = await readPackageManifest.bind(this)(appPath);
return usesPermissions.some((/** @type {string} */ name) => name === 'android.permission.INTERNET');
}

// #region Private functions

/**
* Retrieve the path to the recent installed Android platform.
*
* @param {string} sdkRoot
* @return {Promise<import('./types').PlatformInfo>} The resulting path to the newest installed platform.
*/
export async function getAndroidPlatformAndPath (sdkRoot) {
const propsPaths = await fs.glob('*/build.prop', {
cwd: path.resolve(sdkRoot, 'platforms'),
absolute: true,
});
/** @type {Record<string, import('./types').PlatformInfo>} */
const platformsMapping = {};
for (const propsPath of propsPaths) {
const propsContent = await fs.readFile(propsPath, 'utf-8');
const platformPath = path.dirname(propsPath);
const platform = path.basename(platformPath);
const match = /ro\.build\.version\.sdk=(\d+)/.exec(propsContent);
if (!match) {
log.warn(`Cannot read the SDK version from '${propsPath}'. Skipping '${platform}'`);
continue;
}
platformsMapping[parseInt(match[1], 10)] = {
platform,
platformPath,
};
}
if (_.isEmpty(platformsMapping)) {
log.warn(`Found zero platform folders at '${path.resolve(sdkRoot, 'platforms')}'. ` +
`Do you have any Android SDKs installed?`);
return {
platform: null,
platformPath: null,
};
}

const recentSdkVersion = _.keys(platformsMapping).sort().reverse()[0];
const result = platformsMapping[recentSdkVersion];
log.debug(`Found the most recent Android platform: ${JSON.stringify(result)}`);
return result;
}

// #endregion
64 changes: 60 additions & 4 deletions lib/tools/apk-signing.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import _fs from 'fs';
import { exec } from 'teen_process';
import path from 'path';
import { log } from '../logger.js';
import { tempDir, system, mkdirp, fs, util } from '@appium/support';
import { tempDir, system, mkdirp, fs, util, zip } from '@appium/support';
import { LRUCache } from 'lru-cache';
import {
getJavaForOs, getApksignerForOs, getJavaHome, getResourcePath, APKS_EXTENSION, unsignApk,
getJavaForOs,
getJavaHome,
APKS_EXTENSION,
getResourcePath,
} from '../helpers.js';

const DEFAULT_PRIVATE_KEY = path.join('keys', 'testkey.pk8');
Expand Down Expand Up @@ -36,7 +39,7 @@ const SIGNED_APPS_CACHE = new LRUCache({
* or the return code is not equal to zero.
*/
export async function executeApksigner (args) {
const apkSignerJar = await getApksignerForOs(this);
const apkSignerJar = await getApksignerForOs.bind(this)();
const fullCmd = [
await getJavaForOs(), '-Xmx1024M', '-Xss1m',
'-jar', apkSignerJar,
Expand Down Expand Up @@ -269,7 +272,7 @@ export async function checkApkCert (appPath, pkg, opts = {}) {

const expected = this.useKeystore ? await this.getKeystoreHash() : DEFAULT_CERT_HASH;
try {
await getApksignerForOs(this);
await getApksignerForOs.bind(this)();
const output = await this.executeApksigner(['verify', '--print-certs', appPath]);
const hasMatch = hashMatches(output, expected);
if (hasMatch) {
Expand Down Expand Up @@ -360,3 +363,56 @@ export async function getKeystoreHash () {
`Original error: ${e.stderr || e.message}`);
}
}

// #region Private functions

/**
* Get the absolute path to apksigner tool
*
* @this {import('../adb').ADB}
* @returns {Promise<string>} An absolute path to apksigner tool.
* @throws {Error} If the tool is not present on the local file system.
*/
export async function getApksignerForOs () {
return await this.getBinaryFromSdkRoot('apksigner.jar');
}

/**
* Unsigns the given apk by removing the
* META-INF folder recursively from the archive.
* !!! The function overwrites the given apk after successful unsigning !!!
*
* @param {string} apkPath The path to the apk
* @returns {Promise<boolean>} `true` if the apk has been successfully
* unsigned and overwritten
* @throws {Error} if there was an error during the unsign operation
*/
export async function unsignApk (apkPath) {
const tmpRoot = await tempDir.openDir();
const metaInfFolderName = 'META-INF';
try {
let hasMetaInf = false;
await zip.readEntries(apkPath, ({entry}) => {
hasMetaInf = entry.fileName.startsWith(`${metaInfFolderName}/`);
// entries iteration stops after `false` is returned
return !hasMetaInf;
});
if (!hasMetaInf) {
return false;
}
const tmpZipRoot = path.resolve(tmpRoot, 'apk');
await zip.extractAllTo(apkPath, tmpZipRoot);
await fs.rimraf(path.resolve(tmpZipRoot, metaInfFolderName));
const tmpResultPath = path.resolve(tmpRoot, path.basename(apkPath));
await zip.toArchive(tmpResultPath, {
cwd: tmpZipRoot,
});
await fs.unlink(apkPath);
await fs.mv(tmpResultPath, apkPath);
return true;
} finally {
await fs.rimraf(tmpRoot);
}
}

// #endregion
230 changes: 229 additions & 1 deletion lib/tools/apk-utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
APKS_EXTENSION, buildInstallArgs,
APK_INSTALL_TIMEOUT, DEFAULT_ADB_EXEC_TIMEOUT,
parseAaptStrings, parseAapt2Strings, formatConfigMarker,
readPackageManifest
} from '../helpers.js';
import { exec } from 'teen_process';
Expand Down Expand Up @@ -530,3 +529,232 @@ export async function getApkInfo (appPath) {
}
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();
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)) {
log.debug(`Resource configuration name '${configMarker}' is unknown. ` +
`Replacing it with '${defaultMarker}'`);
configMarker = defaultMarker;
} else {
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.
*/
export function parseAapt2Strings (rawOutput, configMarker) {
const allLines = rawOutput.split(os.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${_.trimStart(allLines[idx].substring(0, terminationCharPos))}`,
idx,
];
}
if (idx > startIdx) {
result += `\\n${_.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 (_.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 (_.isString(content)) {
apkStrings[currentResourceId] = [
...(apkStrings[currentResourceId] || []),
content,
];
}
}
} else if (trimmedLine.startsWith(`(${configMarker})`)) {
const [content, idx] = extractContent(lineIndex);
lineIndex = idx;
if (_.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.
*/
export 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.EOL)) {
const trimmedLine = line.trim();
if (_.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
Loading

0 comments on commit 18e7919

Please sign in to comment.