Skip to content

Commit

Permalink
Merge pull request #148 from DeployGate/jmatsu/feat/aapt2_include_met…
Browse files Browse the repository at this point in the history
…a_data

Let android_apk object return meta data tags
  • Loading branch information
jmatsu authored Mar 15, 2024
2 parents 88f18cf + 30a1fa5 commit c552636
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 132 deletions.
29 changes: 12 additions & 17 deletions lib/android_apk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
require_relative "android_apk/aapt2/dump_badging"
require_relative "android_apk/aapt2/dump_resources"

# @!attribute [r] aapt2_badging_result
# @return [AndroidApk::Aapt2::DumpBadging::Result]
# @!attribute [r] label
# @return [String, NilClass] Return a value which is defined in AndroidManifest.xml. Could be nil.
# @!attribute [r] default_icon_path
Expand All @@ -42,6 +44,10 @@
# @deprecated no longer used
# @!attribute [r] icon_path_hash
# @return [Hash] Application icon paths for all densities that are human readable names. This value may contains anyapi-v<api_version>.
# @!attribute [r] app_signature
# @return [AndroidApk::AppSignature] An object contains lineages and certificate fingerprints
# @!attribute [r] meta_data
# @return [Hash] Named hash of meta-data tags in AndroidManifest.xml. Return an empty if none is found.
class AndroidApk
FALLBACK_DPI = 65_534
ADAPTIVE_ICON_SDK = 26
Expand All @@ -56,12 +62,6 @@ def logger
end
end

NOT_ALLOW_DUPLICATE_TAG_NAMES = %w(
application
sdkVersion
targetSdkVersion
).freeze

DPI_TO_NAME_MAP = {
120 => "ldpi",
160 => "mdpi",
Expand All @@ -83,25 +83,20 @@ module Reason

extend Forwardable

delegate %i(label default_icon_path test_only test_only? package_name version_code version_name min_sdk_version target_sdk_version icons labels) => :@aapt2_result

alias icon default_icon_path
attr_reader :aapt2_badging_result, :icon_path_hash, :app_signature

attr_reader :icon_path_hash
delegate %i(label default_icon_path test_only test_only? package_name version_code version_name min_sdk_version target_sdk_version icons labels meta_data) => :@aapt2_badging_result

alias icon default_icon_path
alias sdk_version min_sdk_version

# An object contains lineages and certificate fingerprints
# @return [AndroidApk::AppSignature] a signature representation
attr_reader :app_signature

# Do analyze the given apk file. Analyzed apk does not mean *valid*.
#
# @param [String] filepath a filepath of an apk to be analyzed
# @return [AndroidApk] An instance of AndroidApk will be returned if no problem exists while analyzing.
# @raise [AndroidApk::ApkFileNotFoundError] if the filepath doesn't exist
# @raise [AndroidApk::UnacceptableApkError] if the apk file is not acceptable by commands like aapt
# @raise [AndroidApk::AndroidManifestValidateError] if the apk contains invalid AndroidManifest.xml but only when we can identify why it's invalid.
# @raise [AndroidApk::Aapt2Error] if the apk file is not acceptable by commands like aapt
# @raise [AndroidApk::InvalidApkError] if the apk contains invalid AndroidManifest.xml but only when we can identify why it's invalid.
def self.analyze(filepath)
AndroidApk.new(
filepath: filepath
Expand All @@ -114,7 +109,7 @@ def initialize(
raise ApkFileNotFoundError, "an apk file is required to analyze." unless File.exist?(filepath)

@filepath = filepath
@aapt2_result = Aapt2::DumpBadging.new(apk_filepath: filepath).parse
@aapt2_badging_result = Aapt2::DumpBadging.new(apk_filepath: filepath).parse

# It seems the resources in the aapt's output doesn't mean that it's available in resource.arsc
icons_in_arsc = ::AndroidApk::ResourceFinder.decode_resource_table(
Expand Down
221 changes: 138 additions & 83 deletions lib/android_apk/aapt2/dump_badging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,120 +3,171 @@
class AndroidApk
module Aapt2
class DumpBadging
# @!attribute [r] label
# @return [String, NilClass] Return a value which is defined in AndroidManifest.xml. Could be nil.
# @!attribute [r] default_icon_path
# @return [String] Return a relative path of this apk's icon. This is the real filepath in the apk but not resource-friendly path.
# @!attribute [r] test_only
# @return [Boolean] Check whether or not this apk is a test mode. Return true if an apk is a test apk
# @!attribute [r] package_name
# @return [String] an application's package name which is defined in AndroidManifest
# @!attribute [r] version_code
# @return [String] an application's version code which is defined in AndroidManifest
# @!attribute [r] version_name
# @return [String] an application's version name which is defined in AndroidManifest
# @!attribute [r] min_sdk_version
# @return [String, NilClass] an application's min sdk version. The format is an integer string which is defined in AndroidManifest.xml. Legacy apk may return nil.
# @!attribute [r] target_sdk_version
# @return [String, NilClass] an application's target sdk version. The format is an integer string which is defined in AndroidManifest.xml. Legacy apk may return nil.
# @!attribute [r] labels
# @return [Hash] an application's labels a.k.a application name in available resources.
# @!attribute [r] icons
# @return [Hash] an application's relative icon paths grouped by densities
# @deprecated no longer used
# @!attribute [r] raw_result_lines
# @return [Array<String>] Raw outputs of aapt2 dump badging
class Result
attr_reader :label, :default_icon_path, :test_only, :package_name, :version_code, :version_name, :min_sdk_version, :target_sdk_version, :icons, :labels
attr_reader :raw_result_lines

def initialize(variables)
# application info
@label = variables["application-label"]
def initialize(raw_result_lines:, parsed_variables:)
@raw_result_lines = raw_result_lines
@parsed_variables = parsed_variables
end

@default_icon_path = variables["application"]["icon"]
@test_only = variables.key?("testOnly='-1'")
# @return [String, NilClass] Return a value which is defined in AndroidManifest.xml. Could be nil.
def label
@parsed_variables["application-label"]
end

# package
# @return [String] Return a relative path of this apk's icon. This is the real filepath in the apk but not resource-friendly path.
def default_icon_path
@parsed_variables["application"]["icon"]
end

@package_name = variables["package"]["name"]
@version_code = variables["package"]["versionCode"]
@version_name = variables["package"]["versionName"] || ""
# @return [String] an application's package name which is defined in AndroidManifest
def package_name
@parsed_variables["package"]["name"]
end

# platforms
@min_sdk_version = variables["sdkVersion"]
@target_sdk_version = variables["targetSdkVersion"]
# @return [String] an application's version code which is defined in AndroidManifest
def version_code
@parsed_variables["package"]["versionCode"]
end

# icons and labels
@icons = {} # old
@labels = {}
# FIXME: don't return empty but return nil as it is cuz it's valid.
# @return [String] an application's version name which is defined in AndroidManifest or empty
def version_name
@parsed_variables["package"]["versionName"] || ""
end

variables.each_key do |k|
if (m = k.match(/\Aapplication-icon-(\d+)\z/))
@icons[m[1].to_i] = variables[k]
elsif (m = k.match(/\Aapplication-label-(\S+)\z/))
@labels[m[1]] = variables[k]
end
end
# @return [String, NilClass] an application's min sdk version. The format is an integer string which is defined in AndroidManifest.xml. Legacy apk may return nil.
def min_sdk_version
@parsed_variables["sdkVersion"]
end

# @return [String, NilClass] an application's target sdk version. The format is an integer string which is defined in AndroidManifest.xml. Legacy apk may return nil.
def target_sdk_version
@parsed_variables["targetSdkVersion"]
end

# @return [Boolean] Check whether or not this apk is a test mode. Return true if an apk is a test apk
def test_only
@parsed_variables.key?("testOnly='-1'")
end

alias test_only? test_only

# @return [Hash<{String => String}>] A hash whose keys are names of meta-data and values are of them.
def meta_data
return @meta_data if defined?(@meta_data)

@meta_data = (@parsed_variables["meta-data"] || []).each_with_object({}) do |h, acc|
acc[h["name"]] = h["value"]
end
end

# @return [Hash] an application's labels a.k.a application name in available resources.
def labels
@parsed_variables["labels"]
end

# @return [Hash] an application's relative icon paths grouped by densities
# @deprecated no longer used
def icons
@parsed_variables["icons"]
end
end

MULTIPLE_ELEMENTS_TAG_NAMES = %w(
meta-data
).freeze
BOOLEAN_ELEMENT_TAG_NAMES = %w(
testOnly='-1'
application-debuggable
).freeze
SINGLE_VALUE_ELEMENT_TAG_NAMES = %w(
application
application-label
package
sdkVersion
targetSdkVersion
).freeze

NOT_ALLOW_DUPLICATE_TAG_NAMES = %w(
application
sdkVersion
targetSdkVersion
).freeze

# @return [String]
def self.dump_badging(apk_filepath:)
stdout, stderr, status = Open3.capture3("aapt2", "dump", "badging", apk_filepath)
stdout if status.success?
stdout, stderr, status = Open3.capture3("aapt2", "dump", "badging", "--include-meta-data", apk_filepath)

if status.success?
stdout
else
if stderr.index(/ERROR:?\s/) # : is never required because it's mixed.
# *normally* failed. The output of aapt2 dump is helpful.
# ref: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/tools/aapt/Command.cpp;l=860?q=%22dump%20failed%22%20aapt
raise UnacceptableApkError, "This apk file cannot be analyzed using 'aapt2 dump badging --include-meta-data'. stdout = #{stdout}, stderr = #{stderr}"
if stderr.index(/ERROR:?\s/i) # : is never required because it's mixed.
if stderr.include?("failed opening zip")
raise InvalidApkError, "This apk file is an invalid zip-format or contains no AndroidManifest.xml"
elsif stderr.include?("failed to parse binary AndroidManifest.xml")
raise InvalidApkError, "AndroidManifest.xml seems to be invalid and not decode-able."
else
# *normally* failed. The output of aapt2 dump is helpful.
# ref: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/tools/aapt/Command.cpp;l=860?q=%22dump%20failed%22%20aapt
raise Aapt2Error.new(message: "This apk file cannot be parsed using 'aapt2 dump badging --include-meta-data'", stdout: stdout, stderr: stderr)
end
else
# unexpectedly failed. This may happen due to the running environment.
raise UnacceptableApkError, "'aapt2 dump badging --include-meta-data' failed due to an unexpected error."
raise Aapt2Error.new(message: "'aapt2 dump badging --include-meta-data' failed due to an unexpected error.", stdout: stdout, stderr: stderr)
end
end
end

# @param apk_filepath [String] a path to apk_filepath
def initialize(apk_filepath:)
@apk_filepath = apk_filepath
@dump_results = self.class.dump_badging(apk_filepath: apk_filepath)&.scrub&.split("\n")
end

# Parse output of aapt2 command to Hash format
#
# @return [::AndroidApk::Aapt2::DumpBadging::Result]
def parse
vars = {}
results = @dump_results.dup
results.each do |line|
key, value = parse_line(line)
next if key.nil?
return @parse if defined?(@parse)

if vars.key?(key)
reject_illegal_duplicated_key!(key)
raw_result_lines = self.class.dump_badging(apk_filepath: @apk_filepath).scrub.split("\n")

if vars[key].kind_of?(Hash) and value.kind_of?(Hash)
vars[key].merge(value)
else
vars[key] = [vars[key]] unless vars[key].kind_of?(Array)
if value.kind_of?(Array)
vars[key].concat(value)
else
vars = {
"labels" => {},
"icons" => {},
"meta-data" => []
}

raw_result_lines.each do |line|
key, value = parse_line(line)

if !(m = key.match(/\Aapplication-icon-(\d+)\z/)).nil?
vars["icons"][m[1].to_i] = value unless value.nil?
elsif !(m = key.match(/\Aapplication-label-(\S+)\z/)).nil?
vars["labels"][m[1]] = value unless value.nil?
else
# noinspection RubyCaseWithoutElseBlockInspection
case key
when *BOOLEAN_ELEMENT_TAG_NAMES
vars[key] = true
when *MULTIPLE_ELEMENTS_TAG_NAMES
if value.kind_of?(Hash)
vars[key].push(value)
else
vars[key].push(*value)
end
when *SINGLE_VALUE_ELEMENT_TAG_NAMES
reject_illegal_duplicated_key!(key) if vars.key?(key)

vars[key] = value
end
else
vars[key] = if value.nil? || value.kind_of?(Hash)
value
else
value.length > 1 ? value : value[0]
end
end
end

Result.new(vars)
@parse = Result.new(raw_result_lines: raw_result_lines, parsed_variables: vars)
end

# workaround for https://code.google.com/p/android/issues/detail?id=160847
Expand Down Expand Up @@ -146,25 +197,29 @@ def parse

# Parse output of a line of aapt command like `key: values`
#
# @param [String, nil] line a line of aapt command.
# @return [[String, Hash], nil] return nil if (see line) is nil. Otherwise the parsed hash will be returned.
# @param [String] line a line of aapt command.
# @return [[String, Hash]] return nil if (see line) is nil. Otherwise the parsed hash will be returned.
private def parse_line(line)
return nil if line.nil?
key, values = line.split(":", 2)

info = line.split(":", 2)
values =
if info[0].start_with?("application-label")
parse_values_workaround info[1]
if key.start_with?("application-label")
parse_values_workaround(values)
else
parse_values info[1]
parse_values(values)
end
return info[0], values

if values.nil? || values.kind_of?(Hash) || values.length > 1
[key, values]
else
[key, values[0]]
end
end

# @param [String] key a key of AndroidManifest.xml
# @raise [AndroidManifestValidateError] if a key is found in (see NOT_ALLOW_DUPLICATE_TAG_NAMES)
# @raise [InvalidApkError] if a key is found in (see NOT_ALLOW_DUPLICATE_TAG_NAMES)
private def reject_illegal_duplicated_key!(key)
raise AndroidManifestValidateError, key if NOT_ALLOW_DUPLICATE_TAG_NAMES.include?(key)
raise InvalidApkError, "duplicates of #{key} tag in AndroidManifest.xml are invalid." if NOT_ALLOW_DUPLICATE_TAG_NAMES.include?(key)
end
end
end
Expand Down
18 changes: 14 additions & 4 deletions lib/android_apk/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@ class AndroidApk
class Error < StandardError; end

class ApkFileNotFoundError < Error; end
class UnacceptableApkError < Error; end

class AndroidManifestValidateError < Error
def initialize(tag)
super("duplicates of #{tag} tag in AndroidManifest.xml are invalid.")
# @!attribute [r] stdout
# @return [String, nil] Standard output of a command
# @!attribute [r] stderr
# @return [String, nil] Standard error of a command
class Aapt2Error < Error
attr_reader :stdout, :stderr

def initialize(message:, stdout:, stderr:)
super(message)
@stdout = stdout
@stderr = stderr
end
end

# This error wIll be thrown if an apk is invalid or contains invalid files.
class InvalidApkError < Error; end

class ApkSignerExecutionError < Error; end
class ParsingSignatureError < Error; end
end
Loading

0 comments on commit c552636

Please sign in to comment.