OpenMiInk/third_party/ci-notary-service/sftpnotary/apkutils.py
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

193 lines
6.9 KiB
Python

# SPDX-License-Identifier: BSD-2-Clause
# SPDX-FileCopyrightText: 2023 KDE e.V.
# SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
import logging
import re
from sftpnotary import config, util
from sftpnotary.exceptions import Error
log = logging.getLogger(__name__)
def getApplicationId(apkOrAabFilePath):
if apkOrAabFilePath.suffix == ".aab":
return getApplicationIdFromAab(apkOrAabFilePath)
elif apkOrAabFilePath.suffix == ".apk":
return getApplicationIdFromApk(apkOrAabFilePath)
raise Error(f"Unsupported file format {apkOrAabFilePath.suffix}")
def getApplicationIdFromAab(aabFilePath):
workingDirectory = aabFilePath.parent
aabFileName = aabFilePath.name
bundletoolJarPath = config.settings.getPath("ApkSigning", "BundletoolJarPath")
command = ["java", "-jar", bundletoolJarPath, "dump", "manifest", "--bundle", aabFileName]
result = util.runCommand(command, cwd=workingDirectory)
if not result.stdout:
raise Error("bundletool did not print the AndroidManifest.xml of the base module")
# XML processing is dangerous; therefore, simply parse the XML as text
# https://docs.python.org/3/library/xml.html#xml-vulnerabilities
for line in result.stdout.splitlines():
# look for a line like <manifest [...] package="org.kde.appname"
line = line.strip()
if line.startswith("<manifest "):
m = re.search(' package="([^"]*)"', line)
if m is not None:
return m.group(1)
# stop looking if manifest element didn't match
break
raise Error("The application ID could not be extracted from the AndroidManifest.xml of the base module")
def getApplicationIdFromApk(apkFilePath):
workingDirectory = apkFilePath.parent
apkFileName = apkFilePath.name
apkAnalyzerPath = config.settings.getPath("ApkSigning", "APKAnalyzerPath")
command = [apkAnalyzerPath, "manifest", "application-id", apkFileName]
result = util.runCommand(command, cwd=workingDirectory)
if not result.stdout:
raise Error("apkanalyzer did not output an application ID")
return result.stdout.splitlines()[0]
# The following is based on the list of languages/translations that can be selected
# on the page "Main store listing" under "Store presence" of Google Play Console
# (as of 2024-03-15). The list is sorted by language (as in Google Play Console).
supportedGooglePlayLocaleIDs = [
"af", # Afrikaans
"sq", # Albanian
"am", # Amharic
"ar", # Arabic
"hy-AM", # Armenian
"az-AZ", # Azerbaijani
"bn-BD", # Bangla
"eu-ES", # Basque
"be", # Belarusian
"bg", # Bulgarian
"my-MM", # Burmese
"ca", # Catalan
"zh-HK", # Chinese (Hong Kong)
"zh-CN", # Chinese (Simplified)
"zh-TW", # Chinese (Traditional)
"hr", # Croatian
"cs-CZ", # Czech
"da-DK", # Danish
"nl-NL", # Dutch
"en-AU", # English (Australia)
"en-CA", # English (Canada)
"en-GB", # English (United Kingdom)
"en-US", # English (United States)
"en-IN", # English
"en-SG", # English
"en-ZA", # English
"et", # Estonian
"fil", # Filipino
"fi-FI", # Finnish
"fr-CA", # French (Canada)
"fr-FR", # French (France)
"gl-ES", # Galician
"ka-GE", # Georgian
"de-DE", # German
"el-GR", # Greek
"gu", # Gujarati
"iw-IL", # Hebrew
"hi-IN", # Hindi
"hu-HU", # Hungarian
"is-IS", # Icelandic
"id", # Indonesian
"it-IT", # Italian
"ja-JP", # Japanese
"kn-IN", # Kannada
"kk", # Kazakh
"km-KH", # Khmer
"ko-KR", # Korean
"ky-KG", # Kyrgyz
"lo-LA", # Lao
"lv", # Latvian
"lt", # Lithuanian
"mk-MK", # Macedonian
"ms-MY", # Malay (Malaysia)
"ms", # Malay
"ml-IN", # Malayalam
"mr-IN", # Marathi
"mn-MN", # Mongolian
"ne-NP", # Nepali
"no-NO", # Norwegian
"fa", # Persian
"fa-AE", # Persian
"fa-AF", # Persian
"fa-IR", # Persian
"pl-PL", # Polish
"pt-BR", # Portuguese (Brazil)
"pt-PT", # Portuguese (Portugal)
"pa", # Punjabi
"ro", # Romanian
"rm", # Romansh
"ru-RU", # Russian
"sr", # Serbian
"si-LK", # Sinhala
"sk", # Slovak
"sl", # Slovenian
"es-419", # Spanish (Latin America)
"es-ES", # Spanish (Spain)
"es-US", # Spanish (United States)
"sw", # Swahili
"sv-SE", # Swedish
"ta-IN", # Tamil
"te-IN", # Telugu
"th", # Thai
"tr-TR", # Turkish
"uk", # Ukrainian
"ur", # Urdu
"vi", # Vietnamese
"zu", # Zulu
]
def filterFastlaneFileNames(names, applicationId):
"""Removes any unexpected file names from the list of files in the Fastlane zip file.
In particular, all folder names (ending with '/') are removed. But that's okay
because when extracting files in those folders the folders will be created anyway.
names should be the return value of ZipFile.namelist().
"""
if not applicationId or "/" in applicationId:
raise ValueError("Invalid application ID: Empty or contains '/'")
# first filter out obviously bad file paths: absolute paths and any
# file paths containing ".." (even inside a file name or folder name);
# this shouldn't be necessary because those paths should never match
# one of the expected names below, but better safe than sorry
filteredNames = [name for name in names if not name.startswith("/") and ".." not in name]
# then filter out everything not matching the expected names
escapedAppId = re.escape(applicationId)
# regular expression for locale IDs supported by the Google Play API
localeID = "(?:" + "|".join(supportedGooglePlayLocaleIDs) + ")"
notEmptyStarGlob = "[^/]+"
expectedNamesREs = [
# <application ID>.yml
re.compile(f"{escapedAppId}\\.yml"),
# files in <application ID>/<language code> folders:
# images/featureGraphic.png, images/icon.png
re.compile(f"{escapedAppId}/{localeID}/images/featureGraphic\\.png"),
re.compile(f"{escapedAppId}/{localeID}/images/icon\\.png"),
# images/phoneScreenshots/*.png
re.compile(f"{escapedAppId}/{localeID}/images/phoneScreenshots/{notEmptyStarGlob}\\.png"),
# full_description.txt, short_description.txt, title.txt
re.compile(f"{escapedAppId}/{localeID}/full_description\\.txt"),
re.compile(f"{escapedAppId}/{localeID}/short_description\\.txt"),
re.compile(f"{escapedAppId}/{localeID}/title\\.txt"),
]
filteredNames = [name for name in filteredNames if any(regex.fullmatch(name) for regex in expectedNamesREs)]
unexpectedNames = sorted(set(names) - set(filteredNames))
unexpectedNames = [n for n in unexpectedNames if not n.endswith("/")] # remove folder names
if unexpectedNames:
log.warning(f"Found unexpected files in the Fastlane zip file: {unexpectedNames}")
return filteredNames