android_parser 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +54 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +22 -0
- data/README.md +158 -0
- data/Rakefile +44 -0
- data/android_parser.gemspec +64 -0
- data/lib/android/apk.rb +220 -0
- data/lib/android/axml_parser.rb +239 -0
- data/lib/android/axml_writer.rb +49 -0
- data/lib/android/dex/access_flag.rb +74 -0
- data/lib/android/dex/dex_object.rb +475 -0
- data/lib/android/dex/info.rb +151 -0
- data/lib/android/dex/utils.rb +45 -0
- data/lib/android/dex.rb +92 -0
- data/lib/android/layout.rb +44 -0
- data/lib/android/manifest.rb +350 -0
- data/lib/android/resource.rb +621 -0
- data/lib/android/utils.rb +55 -0
- data/lib/ruby_apk.rb +8 -0
- metadata +193 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: ruby_apk 2.3.0 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "android_parser".freeze
|
9
|
+
s.version = "2.4.1"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib".freeze]
|
13
|
+
s.authors = ["SecureBrain".freeze]
|
14
|
+
s.date = "2021-08-04"
|
15
|
+
s.description = "static analysis tool for android apk".freeze
|
16
|
+
s.email = "info@securebrain.co.jp".freeze
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"CHANGELOG.md",
|
19
|
+
"LICENSE.txt",
|
20
|
+
"README.md"
|
21
|
+
]
|
22
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
s.homepage = "https://github.com/icyleaf/ruby_apk".freeze
|
24
|
+
s.licenses = ["MIT".freeze]
|
25
|
+
s.rubygems_version = "3.0.3".freeze
|
26
|
+
s.summary = "static analysis tool for android apk".freeze
|
27
|
+
|
28
|
+
if s.respond_to? :specification_version then
|
29
|
+
s.specification_version = 4
|
30
|
+
|
31
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
32
|
+
s.add_runtime_dependency(%q<rubyzip>.freeze, [">= 1.0.0"])
|
33
|
+
s.add_development_dependency(%q<rspec-its>.freeze, [">= 1.2.0"])
|
34
|
+
s.add_development_dependency(%q<rspec-collection_matchers>.freeze, [">= 1.1.0"])
|
35
|
+
s.add_development_dependency(%q<rspec-mocks>.freeze, [">= 3.6.0"])
|
36
|
+
s.add_development_dependency(%q<bundler>.freeze, [">= 1.1.5"])
|
37
|
+
s.add_development_dependency(%q<jeweler>.freeze, [">= 0"])
|
38
|
+
s.add_development_dependency(%q<yard>.freeze, [">= 0"])
|
39
|
+
s.add_development_dependency(%q<redcarpet>.freeze, [">= 0"])
|
40
|
+
s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
|
41
|
+
else
|
42
|
+
s.add_dependency(%q<rubyzip>.freeze, [">= 1.0.0"])
|
43
|
+
s.add_dependency(%q<rspec-its>.freeze, [">= 1.2.0"])
|
44
|
+
s.add_dependency(%q<rspec-collection_matchers>.freeze, [">= 1.1.0"])
|
45
|
+
s.add_dependency(%q<rspec-mocks>.freeze, [">= 3.6.0"])
|
46
|
+
s.add_dependency(%q<bundler>.freeze, [">= 1.1.5"])
|
47
|
+
s.add_dependency(%q<jeweler>.freeze, [">= 0"])
|
48
|
+
s.add_dependency(%q<yard>.freeze, [">= 0"])
|
49
|
+
s.add_dependency(%q<redcarpet>.freeze, [">= 0"])
|
50
|
+
s.add_dependency(%q<simplecov>.freeze, [">= 0"])
|
51
|
+
end
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<rubyzip>.freeze, [">= 1.0.0"])
|
54
|
+
s.add_dependency(%q<rspec-its>.freeze, [">= 1.2.0"])
|
55
|
+
s.add_dependency(%q<rspec-collection_matchers>.freeze, [">= 1.1.0"])
|
56
|
+
s.add_dependency(%q<rspec-mocks>.freeze, [">= 3.6.0"])
|
57
|
+
s.add_dependency(%q<bundler>.freeze, [">= 1.1.5"])
|
58
|
+
s.add_dependency(%q<jeweler>.freeze, [">= 0"])
|
59
|
+
s.add_dependency(%q<yard>.freeze, [">= 0"])
|
60
|
+
s.add_dependency(%q<redcarpet>.freeze, [">= 0"])
|
61
|
+
s.add_dependency(%q<simplecov>.freeze, [">= 0"])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
data/lib/android/apk.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'zip' # need rubyzip gem -> doc: http://rubyzip.sourceforge.net/
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
module Android
|
8
|
+
class NotApkFileError < StandardError; end
|
9
|
+
class NotFoundError < StandardError; end
|
10
|
+
|
11
|
+
# apk object class
|
12
|
+
class Apk
|
13
|
+
|
14
|
+
# @return [String] apk file path
|
15
|
+
attr_reader :path
|
16
|
+
# @return [Android::Manifest] manifest instance
|
17
|
+
# @return [nil] when parsing manifest is failed.
|
18
|
+
attr_reader :manifest
|
19
|
+
# @return [Android::Dex] dex instance
|
20
|
+
# @return [nil] when parsing dex is failed.
|
21
|
+
attr_reader :dex
|
22
|
+
# @return [String] binary data of apk
|
23
|
+
attr_reader :bindata
|
24
|
+
# @return [Resource] resouce data
|
25
|
+
# @return [nil] when parsing resource is failed.
|
26
|
+
attr_reader :resource
|
27
|
+
|
28
|
+
# AndroidManifest file name
|
29
|
+
MANIFEST = 'AndroidManifest.xml'
|
30
|
+
# dex file name
|
31
|
+
DEX = 'classes.dex'
|
32
|
+
# resource file name
|
33
|
+
RESOURCE = 'resources.arsc'
|
34
|
+
|
35
|
+
# create new apk object
|
36
|
+
# @param [String] filepath apk file path
|
37
|
+
# @raise [Android::NotFoundError] path file does'nt exist
|
38
|
+
# @raise [Android::NotApkFileError] path file is not Apk file.
|
39
|
+
def initialize(filepath)
|
40
|
+
@path = filepath
|
41
|
+
raise NotFoundError, "'#{filepath}'" unless File.exist? @path
|
42
|
+
begin
|
43
|
+
@zip = Zip::File.open(@path)
|
44
|
+
rescue Zip::Error => e
|
45
|
+
raise NotApkFileError, e.message
|
46
|
+
end
|
47
|
+
|
48
|
+
@bindata = File.open(@path, 'rb') {|f| f.read }
|
49
|
+
@bindata.force_encoding(Encoding::ASCII_8BIT)
|
50
|
+
raise NotApkFileError, "manifest file is not found." if @zip.find_entry(MANIFEST).nil?
|
51
|
+
begin
|
52
|
+
@resource = Android::Resource.new(self.file(RESOURCE))
|
53
|
+
rescue => e
|
54
|
+
$stderr.puts "failed to parse resource:#{e}"
|
55
|
+
#$stderr.puts e.backtrace
|
56
|
+
end
|
57
|
+
begin
|
58
|
+
@manifest = Android::Manifest.new(self.file(MANIFEST), @resource)
|
59
|
+
rescue => e
|
60
|
+
$stderr.puts "failed to parse manifest:#{e}"
|
61
|
+
#$stderr.puts e.backtrace
|
62
|
+
end
|
63
|
+
begin
|
64
|
+
@dex = Android::Dex.new(self.file(DEX))
|
65
|
+
rescue => e
|
66
|
+
$stderr.puts "failed to parse dex:#{e}"
|
67
|
+
#$stderr.puts e.backtrace
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# return apk file size
|
72
|
+
# @return [Integer] bytes
|
73
|
+
def size
|
74
|
+
@bindata.size
|
75
|
+
end
|
76
|
+
|
77
|
+
# return hex digest string of apk file
|
78
|
+
# @param [Symbol] type hash digest type(:sha1, sha256, :md5)
|
79
|
+
# @return [String] hex digest string
|
80
|
+
# @raise [ArgumentError] type is knknown type
|
81
|
+
def digest(type = :sha1)
|
82
|
+
case type
|
83
|
+
when :sha1
|
84
|
+
Digest::SHA1.hexdigest(@bindata)
|
85
|
+
when :sha256
|
86
|
+
Digest::SHA256.hexdigest(@bindata)
|
87
|
+
when :md5
|
88
|
+
Digest::MD5.hexdigest(@bindata)
|
89
|
+
else
|
90
|
+
raise ArgumentError
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# returns date of AndroidManifest.xml as Apk date
|
95
|
+
# @return [Time]
|
96
|
+
def time
|
97
|
+
entry(MANIFEST).time
|
98
|
+
end
|
99
|
+
|
100
|
+
# @yield [name, data]
|
101
|
+
# @yieldparam [String] name file name in apk
|
102
|
+
# @yieldparam [String] data file data in apk
|
103
|
+
def each_file
|
104
|
+
@zip.each do |entry|
|
105
|
+
next unless entry.file?
|
106
|
+
yield entry.name, @zip.read(entry)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# find and return binary data with name
|
111
|
+
# @param [String] name file name in apk(fullpath)
|
112
|
+
# @return [String] binary data
|
113
|
+
# @raise [NotFoundError] when 'name' doesn't exist in the apk
|
114
|
+
def file(name) # get data by entry name(path)
|
115
|
+
@zip.read(entry(name))
|
116
|
+
end
|
117
|
+
|
118
|
+
# @yield [entry]
|
119
|
+
# @yieldparam [Zip::Entry] entry zip entry
|
120
|
+
def each_entry
|
121
|
+
@zip.each do |entry|
|
122
|
+
next unless entry.file?
|
123
|
+
yield entry
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# find and return zip entry with name
|
128
|
+
# @param [String] name file name in apk(fullpath)
|
129
|
+
# @return [Zip::Entry] zip entry object
|
130
|
+
# @raise [NotFoundError] when 'name' doesn't exist in the apk
|
131
|
+
def entry(name)
|
132
|
+
entry = @zip.find_entry(name)
|
133
|
+
raise NotFoundError, "'#{name}'" if entry.nil?
|
134
|
+
return entry
|
135
|
+
end
|
136
|
+
|
137
|
+
# find files which is matched with block condition
|
138
|
+
# @yield [name, data] find condition
|
139
|
+
# @yieldparam [String] name file name in apk
|
140
|
+
# @yieldparam [String] data file data in apk
|
141
|
+
# @yieldreturn [Array] Array of matched entry name
|
142
|
+
# @return [Array] Array of matched entry name
|
143
|
+
# @example
|
144
|
+
# apk = Apk.new(path)
|
145
|
+
# elf_files = apk.find { |name, data| data[0..3] == [0x7f, 0x45, 0x4c, 0x46] } # ELF magic number
|
146
|
+
def find(&block)
|
147
|
+
found = []
|
148
|
+
self.each_file do |name, data|
|
149
|
+
ret = block.call(name, data)
|
150
|
+
found << name if ret
|
151
|
+
end
|
152
|
+
found
|
153
|
+
end
|
154
|
+
|
155
|
+
# extract application icon data from AndroidManifest and resource.
|
156
|
+
# @return [Hash{ String => String }] hash key is icon filename. value is image data
|
157
|
+
# @raise [NotFoundError]
|
158
|
+
# @since 0.6.0
|
159
|
+
def icon
|
160
|
+
icon_id = @manifest.doc.elements['/manifest/application'].attributes['icon']
|
161
|
+
icon_by_id(icon_id)
|
162
|
+
end
|
163
|
+
|
164
|
+
# extract icon data from AndroidManifest and resource by a given icon id.
|
165
|
+
# @param [String] icon_id to be searched in the resource.
|
166
|
+
# @return [Hash{ String => String }] hash key is icon filename. value is image data
|
167
|
+
# @raise [NotFoundError]
|
168
|
+
# @since 0.6.0
|
169
|
+
def icon_by_id(icon_id)
|
170
|
+
if /^@(\w+\/\w+)|(0x[0-9a-fA-F]{8})$/ =~ icon_id
|
171
|
+
drawables = @resource.find(icon_id)
|
172
|
+
Hash[drawables.map {|name| [name, file(name)] }]
|
173
|
+
else
|
174
|
+
begin
|
175
|
+
{ icon_id => file(icon_id) } # ugh!: not tested!!
|
176
|
+
rescue NotFoundError
|
177
|
+
{}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# get application label from AndroidManifest and resources.
|
183
|
+
# @param [String] lang language code like 'ja', 'cn', ...
|
184
|
+
# @return [String] application label string
|
185
|
+
# @return [nil] when label is not found
|
186
|
+
# @deprecated move to {Android::Manifest#label}
|
187
|
+
# @since 0.6.0
|
188
|
+
def label(lang=nil)
|
189
|
+
@manifest.label
|
190
|
+
end
|
191
|
+
|
192
|
+
# get screen layout xml datas
|
193
|
+
# @return [Hash{ String => Android::Layout }] key: laytout file path, value: layout object
|
194
|
+
# @since 0.6.0
|
195
|
+
def layouts
|
196
|
+
@layouts ||= Layout.collect_layouts(self) # lazy parse
|
197
|
+
end
|
198
|
+
|
199
|
+
# apk's signature information
|
200
|
+
# @return [Hash{ String => OpenSSL::PKCS7 } ] key: sign file path, value: signature
|
201
|
+
# @since 0.7.0
|
202
|
+
def signs
|
203
|
+
signs = {}
|
204
|
+
self.each_file do |path, data|
|
205
|
+
# find META-INF/xxx.{RSA|DSA}
|
206
|
+
next unless path =~ /^META-INF\// && data.unpack("CC") == [0x30, 0x82]
|
207
|
+
signs[path] = OpenSSL::PKCS7.new(data)
|
208
|
+
end
|
209
|
+
signs
|
210
|
+
end
|
211
|
+
|
212
|
+
# certificate info which is used for signing
|
213
|
+
# @return [Hash{String => OpenSSL::X509::Certificate }] key: sign file path, value: first certficate in the sign file
|
214
|
+
# @since 0.7.0
|
215
|
+
def certificates
|
216
|
+
return Hash[self.signs.map{|path, sign| [path, sign.certificates.first] }]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
|
5
|
+
module Android
|
6
|
+
# binary AXML parser
|
7
|
+
# @see https://android.googlesource.com/platform/frameworks/base.git Android OS frameworks source
|
8
|
+
# @note
|
9
|
+
# refer to Android OS framework code:
|
10
|
+
#
|
11
|
+
# /frameworks/base/include/androidfw/ResourceTypes.h,
|
12
|
+
#
|
13
|
+
# /frameworks/base/libs/androidfw/ResourceTypes.cpp
|
14
|
+
class AXMLParser
|
15
|
+
def self.axml?(data)
|
16
|
+
(data[0..3] == "\x03\x00\x08\x00")
|
17
|
+
end
|
18
|
+
|
19
|
+
# axml parse error
|
20
|
+
class ReadError < StandardError; end
|
21
|
+
|
22
|
+
TAG_START_NAMESPACE = 0x00100100
|
23
|
+
TAG_END_NAMESPACE = 0x00100101
|
24
|
+
TAG_START = 0x00100102
|
25
|
+
TAG_END = 0x00100103
|
26
|
+
TAG_TEXT = 0x00100104
|
27
|
+
TAG_CDSECT = 0x00100105
|
28
|
+
TAG_ENTITY_REF = 0x00100106
|
29
|
+
|
30
|
+
VAL_TYPE_NULL =0
|
31
|
+
VAL_TYPE_REFERENCE =1
|
32
|
+
VAL_TYPE_ATTRIBUTE =2
|
33
|
+
VAL_TYPE_STRING =3
|
34
|
+
VAL_TYPE_FLOAT =4
|
35
|
+
VAL_TYPE_DIMENSION =5
|
36
|
+
VAL_TYPE_FRACTION =6
|
37
|
+
VAL_TYPE_INT_DEC =16
|
38
|
+
VAL_TYPE_INT_HEX =17
|
39
|
+
VAL_TYPE_INT_BOOLEAN =18
|
40
|
+
VAL_TYPE_INT_COLOR_ARGB8 =28
|
41
|
+
VAL_TYPE_INT_COLOR_RGB8 =29
|
42
|
+
VAL_TYPE_INT_COLOR_ARGB4 =30
|
43
|
+
VAL_TYPE_INT_COLOR_RGB4 =31
|
44
|
+
|
45
|
+
# @return [Array<String>] strings defined in axml
|
46
|
+
attr_reader :strings, :metadata
|
47
|
+
|
48
|
+
# @param [String] axml binary xml data
|
49
|
+
def initialize(axml)
|
50
|
+
@io = StringIO.new(axml, "rb")
|
51
|
+
@strings = []
|
52
|
+
end
|
53
|
+
|
54
|
+
# parse binary xml
|
55
|
+
# @return [REXML::Document]
|
56
|
+
def parse
|
57
|
+
@doc = REXML::Document.new
|
58
|
+
@doc << REXML::XMLDecl.new
|
59
|
+
|
60
|
+
@num_str = word(4*4)
|
61
|
+
@xml_offset = word(3*4)
|
62
|
+
|
63
|
+
@parents = [@doc]
|
64
|
+
@namespaces = []
|
65
|
+
@metadata = []
|
66
|
+
parse_strings
|
67
|
+
parse_tags
|
68
|
+
@doc
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# read one word(4byte) as integer
|
73
|
+
# @param [Integer] offset offset from top position. current position is used if ofset is nil
|
74
|
+
# @return [Integer] little endian word value
|
75
|
+
def word(offset=nil)
|
76
|
+
@io.pos = offset unless offset.nil?
|
77
|
+
@io.read(4).unpack("V")[0]
|
78
|
+
end
|
79
|
+
|
80
|
+
# read 2byte as short integer
|
81
|
+
# @param [Integer] offset offset from top position. current position is used if ofset is nil
|
82
|
+
# @return [Integer] little endian unsign short value
|
83
|
+
def short(offset)
|
84
|
+
@io.pos = offset unless offset.nil?
|
85
|
+
@io.read(2).unpack("v")[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
# relace string table parser
|
89
|
+
def parse_strings
|
90
|
+
strpool = Resource::ResStringPool.new(@io.string, 8) # ugh!
|
91
|
+
@strings = strpool.strings
|
92
|
+
end
|
93
|
+
|
94
|
+
# parse tag
|
95
|
+
def parse_tags
|
96
|
+
# skip until first TAG_START_NAMESPACE
|
97
|
+
pos = @xml_offset
|
98
|
+
pos += 4 until (word(pos) == TAG_START_NAMESPACE)
|
99
|
+
@io.pos -= 4
|
100
|
+
|
101
|
+
# read tags
|
102
|
+
#puts "start tag parse: %d(%#x)" % [@io.pos, @io.pos]
|
103
|
+
until @io.eof?
|
104
|
+
last_pos = @io.pos
|
105
|
+
tag, tag1, line, tag3, ns_id, name_id = @io.read(4*6).unpack("V*")
|
106
|
+
case tag
|
107
|
+
when TAG_START
|
108
|
+
tag6, num_attrs, tag8 = @io.read(4*3).unpack("V*")
|
109
|
+
|
110
|
+
prefix = ''
|
111
|
+
if ns_id != 0xFFFFFFFF
|
112
|
+
namespace_uri = @strings[ns_id]
|
113
|
+
prefix = get_namespace_prefix(namespace_uri) + ':'
|
114
|
+
end
|
115
|
+
elem = REXML::Element.new(prefix + @strings[name_id])
|
116
|
+
|
117
|
+
meta_tag = if @strings[name_id] == 'meta-data'
|
118
|
+
@metadata << {}
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
# If this element is a direct descendent of a namespace declaration
|
123
|
+
# we add the namespace definition as an attribute.
|
124
|
+
if @namespaces.last[:nesting_level] == current_nesting_level
|
125
|
+
elem.add_namespace(@namespaces.last[:prefix], @namespaces.last[:uri])
|
126
|
+
end
|
127
|
+
#puts "start tag %d(%#x): #{@strings[name_id]} attrs:#{num_attrs}" % [last_pos, last_pos]
|
128
|
+
@parents.last.add_element elem
|
129
|
+
num_attrs.times do
|
130
|
+
key, val, type = parse_attribute
|
131
|
+
|
132
|
+
if meta_tag
|
133
|
+
@metadata.last[key] = {
|
134
|
+
value: val,
|
135
|
+
position: @io.pos - 4,
|
136
|
+
val_str_id: @io.pos - 12,
|
137
|
+
is_string: type == VAL_TYPE_STRING
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
if val.is_a?(String)
|
142
|
+
# drop invalid chars that would be rejected by REXML from string
|
143
|
+
val = val.scan(REXML::Text::VALID_XML_CHARS).join
|
144
|
+
end
|
145
|
+
elem.add_attribute(key, val)
|
146
|
+
end
|
147
|
+
@parents.push elem
|
148
|
+
when TAG_END
|
149
|
+
@parents.pop
|
150
|
+
when TAG_END_NAMESPACE
|
151
|
+
@namespaces.pop
|
152
|
+
break if @namespaces.empty? # if the topmost namespace (usually 'android:') has been closed, we‘re done.
|
153
|
+
when TAG_TEXT
|
154
|
+
text = REXML::Text.new(@strings[ns_id])
|
155
|
+
@parents.last.text = text
|
156
|
+
dummy = @io.read(4*1).unpack("V*") # skip 4bytes
|
157
|
+
when TAG_START_NAMESPACE
|
158
|
+
prefix = @strings[ns_id]
|
159
|
+
uri = @strings[name_id]
|
160
|
+
@namespaces.push({ prefix: prefix, uri: uri, nesting_level: current_nesting_level })
|
161
|
+
when TAG_CDSECT
|
162
|
+
raise ReadError, "TAG_CDSECT not implemented"
|
163
|
+
when TAG_ENTITY_REF
|
164
|
+
raise ReadError, "TAG_ENTITY_REF not implemented"
|
165
|
+
else
|
166
|
+
raise ReadError, "pos=%d(%#x)[tag:%#x]" % [last_pos, last_pos, tag]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# parse attribute of a element
|
172
|
+
def parse_attribute
|
173
|
+
ns_id, name_id, val_str_id, flags, val = @io.read(4*5).unpack("V*")
|
174
|
+
key = @strings[name_id]
|
175
|
+
unless ns_id == 0xFFFFFFFF
|
176
|
+
namespace_uri = @strings[ns_id]
|
177
|
+
prefix = get_namespace_prefix(namespace_uri)
|
178
|
+
key = "#{prefix}:#{key}"
|
179
|
+
end
|
180
|
+
value = convert_value(val_str_id, flags, val)
|
181
|
+
return key, value, (flags >> 24)
|
182
|
+
end
|
183
|
+
|
184
|
+
# find the first declared namespace prefix for a URI
|
185
|
+
def get_namespace_prefix(ns_uri)
|
186
|
+
# a namespace might be given as a URI or as a reference to a previously defined namespace.
|
187
|
+
# E.g. like this:
|
188
|
+
# <tag1 xmlns:android="http://schemas.android.com/apk/res/android">
|
189
|
+
# <tag2 xmlns:n0="android" />
|
190
|
+
# </tag1>
|
191
|
+
|
192
|
+
# Walk recursively through the namespaces to
|
193
|
+
# transitively resolve URIs that just pointed to previous namespace prefixes
|
194
|
+
current_uri = ns_uri
|
195
|
+
@namespaces.reverse.each do |ns|
|
196
|
+
if ns[:prefix] == current_uri
|
197
|
+
# we found a previous namespace declaration that was referenced to by
|
198
|
+
# the current_uri. Proceed with this namespace’s URI and try to see if this
|
199
|
+
# is also just a reference to a previous namespace
|
200
|
+
current_uri = ns[:uri]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# current_uri now contains the URI of the topmost namespace declaration.
|
205
|
+
# We’ll take the prefix of this and return it.
|
206
|
+
@namespaces.reverse.each do |ns|
|
207
|
+
return ns[:prefix] if ns[:uri] == current_uri
|
208
|
+
end
|
209
|
+
raise "Could not resolve URI #{ns_uri} to a namespace prefix"
|
210
|
+
end
|
211
|
+
|
212
|
+
def current_nesting_level
|
213
|
+
@parents.length
|
214
|
+
end
|
215
|
+
|
216
|
+
def convert_value(val_str_id, flags, val)
|
217
|
+
unless val_str_id == 0xFFFFFFFF
|
218
|
+
value = @strings[val_str_id]
|
219
|
+
else
|
220
|
+
type = flags >> 24
|
221
|
+
case type
|
222
|
+
when VAL_TYPE_NULL
|
223
|
+
value = nil
|
224
|
+
when VAL_TYPE_REFERENCE
|
225
|
+
value = "@%#x" % val # refered resource id.
|
226
|
+
when VAL_TYPE_INT_DEC
|
227
|
+
value = val
|
228
|
+
when VAL_TYPE_INT_HEX
|
229
|
+
value = "%#x" % val
|
230
|
+
when VAL_TYPE_INT_BOOLEAN
|
231
|
+
value = ((val == 0xFFFFFFFF) || (val==1)) ? true : false
|
232
|
+
else
|
233
|
+
value = "[%#x, flag=%#x]" % [val, flags]
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|