193 lines
6.9 KiB
Python
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
|