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,621 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module Android
|
5
|
+
# based on Android OS source code
|
6
|
+
# /frameworks/base/include/utils/ResourceTypes.h
|
7
|
+
# @see http://justanapplication.wordpress.com/category/android/android-resources/
|
8
|
+
class Resource
|
9
|
+
class Chunk
|
10
|
+
def initialize(data, offset)
|
11
|
+
data.force_encoding(Encoding::ASCII_8BIT)
|
12
|
+
@data = data
|
13
|
+
@offset = offset
|
14
|
+
exec_parse
|
15
|
+
end
|
16
|
+
def exec_parse
|
17
|
+
@data_io = StringIO.new(@data, 'rb')
|
18
|
+
@data_io.seek(@offset)
|
19
|
+
parse
|
20
|
+
@data_io.close
|
21
|
+
end
|
22
|
+
def read_int32
|
23
|
+
@data_io.read(4).unpack('V')[0]
|
24
|
+
end
|
25
|
+
def read_int16
|
26
|
+
@data_io.read(2).unpack('v')[0]
|
27
|
+
end
|
28
|
+
def read_int8
|
29
|
+
@data_io.read(1).ord
|
30
|
+
end
|
31
|
+
def current_position
|
32
|
+
@data_io.pos
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class ChunkHeader < Chunk
|
37
|
+
attr_reader :type, :header_size, :size
|
38
|
+
private
|
39
|
+
def parse
|
40
|
+
@type = read_int16
|
41
|
+
@header_size = read_int16
|
42
|
+
@size = read_int32
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ResTableHeader < ChunkHeader
|
47
|
+
attr_reader :package_count
|
48
|
+
def parse
|
49
|
+
super
|
50
|
+
@package_count = read_int32
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ResStringPool < ChunkHeader
|
55
|
+
class UnsupportedStringFormatError < StandardError; end
|
56
|
+
|
57
|
+
SORTED_FLAG = 1 << 0
|
58
|
+
UTF8_FLAG = 1 << 8
|
59
|
+
|
60
|
+
attr_reader :strings
|
61
|
+
|
62
|
+
def add_string(str)
|
63
|
+
raise UnsupportedStringFormatError, 'Adding strings in UTF-8 format is not supported yet' if utf8_string_format?
|
64
|
+
|
65
|
+
@data_io = StringIO.new(@data, 'r+b')
|
66
|
+
|
67
|
+
increment_string_count
|
68
|
+
bytes_added = insert_string(str)
|
69
|
+
increment_string_start_offset
|
70
|
+
update_chunk_size(bytes_added)
|
71
|
+
|
72
|
+
@data_io.close
|
73
|
+
[@string_count - 1, bytes_added]
|
74
|
+
end
|
75
|
+
|
76
|
+
def utf8_string_format?
|
77
|
+
(@flags & UTF8_FLAG != 0)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def parse
|
82
|
+
super
|
83
|
+
@string_count = read_int32
|
84
|
+
@style_count = read_int32
|
85
|
+
@flags = read_int32
|
86
|
+
@string_start = read_int32
|
87
|
+
@style_start = read_int32
|
88
|
+
@strings = []
|
89
|
+
@string_count.times do
|
90
|
+
offset = @offset + @string_start + read_int32
|
91
|
+
if utf8_string_format?
|
92
|
+
# read length twice(utf16 length and utf8 length)
|
93
|
+
# const uint16_t* ResStringPool::stringAt(size_t idx, size_t* u16len) const
|
94
|
+
u16len, o16 = ResStringPool.utf8_len(@data[offset, 2])
|
95
|
+
u8len, o8 = ResStringPool.utf8_len(@data[offset+o16, 2])
|
96
|
+
str = @data[offset+o16+o8, u8len]
|
97
|
+
@strings << str.force_encoding(Encoding::UTF_8)
|
98
|
+
else
|
99
|
+
u16len, o16 = ResStringPool.utf16_len(@data[offset, 4])
|
100
|
+
str = @data[offset+o16, u16len*2]
|
101
|
+
str.force_encoding(Encoding::UTF_16LE)
|
102
|
+
@strings << str.encode(Encoding::UTF_8)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def increment_string_count
|
108
|
+
string_count_offset = @offset + 8
|
109
|
+
@string_count = @data[string_count_offset, 4].unpack1('V') + 1
|
110
|
+
@data_io.pos = string_count_offset
|
111
|
+
@data_io.write([@string_count].pack('V'))
|
112
|
+
end
|
113
|
+
|
114
|
+
# Inserts the string into the string data section and updates the string index.
|
115
|
+
# @return [Integer] number of bytes added to the string pool chunk
|
116
|
+
def insert_string(str)
|
117
|
+
bytes = str.codepoints << 0
|
118
|
+
# To keep the alignment we need to pad the new string we're inserting.
|
119
|
+
# In total, we're adding the string bytes + 2 bytes string length + 4 bytes string index.
|
120
|
+
padding = (4 - (bytes.size * 2 + 2 + 4) % 4) % 4
|
121
|
+
padding_bytes = [0] * padding
|
122
|
+
next_string_offset = new_string_offset
|
123
|
+
|
124
|
+
string_bytes = ResStringPool.utf16_str_len(str.codepoints) + bytes.pack('v*') + padding_bytes.pack('C*')
|
125
|
+
|
126
|
+
# Write string data into the string data section.
|
127
|
+
@data.insert(next_string_offset, string_bytes)
|
128
|
+
# Insert new string index entry. The offset needs to be relative to the start of the string data section.
|
129
|
+
@data.insert(last_string_index_offset + 4, [next_string_offset - (@offset + @string_start)].pack('V'))
|
130
|
+
|
131
|
+
# We added the bytes of the string itself + a new string index entry
|
132
|
+
string_bytes.size + 4
|
133
|
+
end
|
134
|
+
|
135
|
+
def last_string_index_offset
|
136
|
+
# The last entry in the string index section is the 4 bytes right before the start
|
137
|
+
# of the string-data section (string_start).
|
138
|
+
@offset + @string_start - 4
|
139
|
+
end
|
140
|
+
|
141
|
+
# Calculates the offset at which to insert new string data.
|
142
|
+
# @return [Integer] offset of the end of the current string data section
|
143
|
+
def new_string_offset
|
144
|
+
last_string_index = @data[last_string_index_offset, 4].unpack1('V')
|
145
|
+
offset = @offset + @string_start + last_string_index
|
146
|
+
|
147
|
+
u16len, o16 = ResStringPool.utf16_len(@data[offset, 4])
|
148
|
+
# To insert a new string at the end of the string section, we need to start at the current
|
149
|
+
# last string entry, and add o16 (number of length bytes), u16len * 2(number of string bytes),
|
150
|
+
# and 2 bytes for the terminating null-bytes.
|
151
|
+
offset + o16 + u16len * 2 + 2
|
152
|
+
end
|
153
|
+
|
154
|
+
def increment_string_start_offset
|
155
|
+
string_start_offset = @offset + 20
|
156
|
+
@string_start = @data[string_start_offset, 4].unpack1('V') + 4
|
157
|
+
|
158
|
+
@data_io.pos = string_start_offset
|
159
|
+
@data_io.write([@string_start].pack('V'))
|
160
|
+
end
|
161
|
+
|
162
|
+
def update_chunk_size(bytes_added)
|
163
|
+
size_offset = @offset + 4
|
164
|
+
@size = @data[size_offset, 4].unpack1('V') + bytes_added
|
165
|
+
|
166
|
+
@data_io.pos = size_offset
|
167
|
+
@data_io.write([@size].pack('V'))
|
168
|
+
end
|
169
|
+
|
170
|
+
# @note refer to /frameworks/base/libs/androidfw/ResourceTypes.cpp
|
171
|
+
# static inline size_t decodeLength(const uint8_t** str)
|
172
|
+
# @param [String] data parse target
|
173
|
+
# @return[Integer, Integer] string length and parsed length
|
174
|
+
def self.utf8_len(data)
|
175
|
+
first, second = data.unpack('CC')
|
176
|
+
if (first & 0x80) != 0
|
177
|
+
return (((first & 0x7F) << 8) + second), 2
|
178
|
+
else
|
179
|
+
return first, 1
|
180
|
+
end
|
181
|
+
end
|
182
|
+
# @note refer to /frameworks/base/libs/androidfw/ResourceTypes.cpp
|
183
|
+
# static inline size_t decodeLength(const char16_t** str)
|
184
|
+
# @param [String] data parse target
|
185
|
+
# @return[Integer, Integer] string length and parsed length
|
186
|
+
def self.utf16_len(data)
|
187
|
+
first, second = data.unpack('vv')
|
188
|
+
if (first & 0x8000) != 0
|
189
|
+
return (((first & 0x7FFF) << 16) + second), 4
|
190
|
+
else
|
191
|
+
return first, 2
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.utf16_str_len(str)
|
196
|
+
[str.size].pack('v')
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class ResTablePackage < ChunkHeader
|
201
|
+
attr_reader :name
|
202
|
+
|
203
|
+
def global_string_pool=(pool)
|
204
|
+
@global_string_pool = pool
|
205
|
+
extract_res_strings
|
206
|
+
end
|
207
|
+
|
208
|
+
# find resource by resource id
|
209
|
+
# @param [String] res_id (like '@0x7f010001' or '@string/key')
|
210
|
+
# @param [Hash] opts option
|
211
|
+
# @option opts [String] :lang language code like 'ja', 'cn'...
|
212
|
+
# @option opts [String] :contry cantry code like 'jp'...
|
213
|
+
# @raise [ArgumentError] invalid id format
|
214
|
+
# @note
|
215
|
+
# This method only support string and drawable resource for now.
|
216
|
+
# @note
|
217
|
+
# Always return nil if assign not string type res id.
|
218
|
+
#
|
219
|
+
def find(res_id, opts={})
|
220
|
+
hex_id = strid2int(res_id)
|
221
|
+
tid = ((hex_id&0xff0000) >>16)
|
222
|
+
key = hex_id&0xffff
|
223
|
+
|
224
|
+
case type(tid)
|
225
|
+
when 'string'
|
226
|
+
return find_res_string(key, opts)
|
227
|
+
when 'drawable', 'mipmap'
|
228
|
+
drawables = []
|
229
|
+
@types[tid].each do |type|
|
230
|
+
unless type[key].nil?
|
231
|
+
drawables << @global_string_pool.strings[type[key].val.data]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
return drawables
|
235
|
+
else
|
236
|
+
nil
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def res_types
|
241
|
+
end
|
242
|
+
def find_res_string(key, opts={})
|
243
|
+
unless opts[:lang].nil?
|
244
|
+
string = @res_strings_lang[opts[:lang]]
|
245
|
+
end
|
246
|
+
unless opts[:contry].nil?
|
247
|
+
string = @res_strings_contry[opts[:contry]]
|
248
|
+
end
|
249
|
+
string = @res_strings_default if string.nil?
|
250
|
+
raise NotFoundError unless string.has_key? key
|
251
|
+
return string[key]
|
252
|
+
end
|
253
|
+
private :find_res_string
|
254
|
+
|
255
|
+
# convert string resource id to fixnum
|
256
|
+
# @param [String] res_id (like '@0x7f010001' or '@string/key')
|
257
|
+
# @return [Fixnum] integer id (like 0x7f010001)
|
258
|
+
# @raise [ArgumentError] invalid format
|
259
|
+
def strid2int(res_id)
|
260
|
+
case res_id
|
261
|
+
when /^@?0x[0-9a-fA-F]{8}$/
|
262
|
+
return res_id.sub(/^@/,'').to_i(16)
|
263
|
+
when /^@?\w+\/\w+/
|
264
|
+
return res_hex_id(res_id).sub(/^@/,'').to_i(16)
|
265
|
+
else
|
266
|
+
raise ArgumentError
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def res_readable_id(hex_id)
|
271
|
+
if hex_id.kind_of? String
|
272
|
+
hex_id = hex_id.sub(/^@/,'').to_i(16)
|
273
|
+
end
|
274
|
+
tid = ((hex_id&0xff0000) >>16)
|
275
|
+
key = hex_id&0xffff
|
276
|
+
raise NotFoundError if !@types.has_key?(tid) || @types[tid][0][key].nil?
|
277
|
+
keyid= @types[tid][0][key].key # ugh!
|
278
|
+
"@#{type(tid)}/#{key(keyid)}"
|
279
|
+
end
|
280
|
+
def res_hex_id(readable_id, opt={})
|
281
|
+
dummy, typestr, keystr = readable_id.match(/^@?(\w+)\/(\w+)$/).to_a
|
282
|
+
tid = type_id(typestr)
|
283
|
+
raise NotFoundError unless @types.has_key?(tid)
|
284
|
+
keyid = @types[tid][0].keys[keystr]
|
285
|
+
raise NotFoundError if keyid.nil?
|
286
|
+
"@0x7f%02x%04x" % [tid, keyid]
|
287
|
+
end
|
288
|
+
|
289
|
+
def type_strings
|
290
|
+
@type_strings.strings
|
291
|
+
end
|
292
|
+
def type(id)
|
293
|
+
type_strings[id-1]
|
294
|
+
end
|
295
|
+
def type_id(str)
|
296
|
+
raise NotFoundError unless type_strings.include? str
|
297
|
+
type_strings.index(str) + 1
|
298
|
+
end
|
299
|
+
def key_strings
|
300
|
+
@key_strings.strings
|
301
|
+
end
|
302
|
+
def key(id)
|
303
|
+
key_strings[id]
|
304
|
+
end
|
305
|
+
def key_id(str)
|
306
|
+
raise NotFoundError unless key_strings.include? str
|
307
|
+
key_strings.index(str)
|
308
|
+
end
|
309
|
+
|
310
|
+
def parse
|
311
|
+
super
|
312
|
+
@id = read_int32
|
313
|
+
@name = @data_io.read(256).force_encoding(Encoding::UTF_16LE)
|
314
|
+
@name.encode!(Encoding::UTF_8).strip!
|
315
|
+
type_strings_offset = read_int32
|
316
|
+
@type_strings = ResStringPool.new(@data, @offset + type_strings_offset)
|
317
|
+
@last_public_type = read_int32
|
318
|
+
key_strings_offset = read_int32
|
319
|
+
@key_strings = ResStringPool.new(@data, @offset + key_strings_offset)
|
320
|
+
@last_public_key = read_int32
|
321
|
+
|
322
|
+
offset = @offset + key_strings_offset + @key_strings.size
|
323
|
+
|
324
|
+
@types = {}
|
325
|
+
@specs = {}
|
326
|
+
while offset < (@offset + @size)
|
327
|
+
type = @data[offset, 2].unpack('v')[0]
|
328
|
+
case type
|
329
|
+
when 0x0201 # RES_TABLE_TYPE_TYPE
|
330
|
+
type = ResTableType.new(@data, offset, self)
|
331
|
+
offset += type.size
|
332
|
+
@types[type.id] = [] if @types[type.id].nil?
|
333
|
+
@types[type.id] << type
|
334
|
+
when 0x0202 # RES_TABLE_TYPE_SPEC_TYPE`
|
335
|
+
spec = ResTableTypeSpec.new(@data, offset)
|
336
|
+
offset += spec.size
|
337
|
+
@specs[spec.id] = [] if @specs[spec.id].nil?
|
338
|
+
@specs[spec.id] << spec
|
339
|
+
else
|
340
|
+
raise "chunk type error: type:%#04x" % type
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
private :parse
|
345
|
+
|
346
|
+
def extract_res_strings
|
347
|
+
@res_strings_lang = {}
|
348
|
+
@res_strings_contry = {}
|
349
|
+
begin
|
350
|
+
type = type_id('string')
|
351
|
+
rescue NotFoundError
|
352
|
+
return
|
353
|
+
end
|
354
|
+
@types[type_id('string')].each do |type|
|
355
|
+
str_hash = {}
|
356
|
+
type.entry_count.times do |i|
|
357
|
+
entry = type[i]
|
358
|
+
if entry.nil?
|
359
|
+
str_hash[i] = nil
|
360
|
+
else
|
361
|
+
str_hash[i] = @global_string_pool.strings[type[i].val.data]
|
362
|
+
end
|
363
|
+
end
|
364
|
+
lang = type.config.locale_lang
|
365
|
+
contry = type.config.locale_contry
|
366
|
+
if lang.nil? && contry.nil?
|
367
|
+
@res_strings_default ||= {}
|
368
|
+
@res_strings_default.merge!(str_hash) { |_key, val1, _val2| val1 }
|
369
|
+
else
|
370
|
+
@res_strings_lang[lang] = str_hash unless lang.nil?
|
371
|
+
@res_strings_contry[contry] = str_hash unless contry.nil?
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
private :extract_res_strings
|
376
|
+
|
377
|
+
def inspect
|
378
|
+
"<ResTablePackage offset:%#08x, size:%#x, name:\"%s\">" % [@offset, @size, @name]
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
class ResTableType < ChunkHeader
|
383
|
+
attr_reader :id, :entry_count, :entry_start, :config
|
384
|
+
attr_reader :keys
|
385
|
+
|
386
|
+
def initialize(data, offset, pkg)
|
387
|
+
@pkg = pkg
|
388
|
+
super(data, offset)
|
389
|
+
end
|
390
|
+
# @param [String] index key name
|
391
|
+
# @param [Fixnum] index key index
|
392
|
+
# @return [ResTableEntry]
|
393
|
+
# @return [ResTableMapEntry]
|
394
|
+
# @return nil if entry index is NO_ENTRY(0xFFFFFFFF)
|
395
|
+
def [](index)
|
396
|
+
@entries[index]
|
397
|
+
end
|
398
|
+
|
399
|
+
def parse
|
400
|
+
super
|
401
|
+
@id = read_int8
|
402
|
+
res0 = read_int8 # must be 0.(maybe 4byte align)
|
403
|
+
res1 = read_int16 # must be 0.(maybe 4byte align)
|
404
|
+
@entry_count = read_int32
|
405
|
+
@entry_start = read_int32
|
406
|
+
@config = ResTableConfig.new(@data, current_position)
|
407
|
+
@data_io.seek(@config.size, IO::SEEK_CUR)
|
408
|
+
|
409
|
+
@entries = []
|
410
|
+
@keys = {}
|
411
|
+
@entry_count.times do |i|
|
412
|
+
entry_index = read_int32
|
413
|
+
if entry_index == ResTableEntry::NO_ENTRY
|
414
|
+
@entries << nil
|
415
|
+
else
|
416
|
+
entry = ResTableEntry.read_entry(@data, @offset + @entry_start + entry_index)
|
417
|
+
@entries << entry
|
418
|
+
@keys[@pkg.key(entry.key)] = i
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
private :parse
|
423
|
+
|
424
|
+
|
425
|
+
def inspect
|
426
|
+
"<ResTableType offset:0x#{@offset.to_s(16)}, id:#{@id}, " +
|
427
|
+
"count:#{@entry_count}, start:0x#{@entry_start.to_s(16)}>"
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
class ResTableConfig < Chunk
|
432
|
+
attr_reader :size, :imei, :locale_lang, :locale_contry, :input
|
433
|
+
attr_reader :screen_input, :version, :screen_config
|
434
|
+
def parse
|
435
|
+
@size = read_int32
|
436
|
+
@imei = read_int32
|
437
|
+
la = @data_io.read(2)
|
438
|
+
@locale_lang = la unless la == "\x00\x00"
|
439
|
+
cn = @data_io.read(2)
|
440
|
+
@locale_contry = cn unless cn == "\x00\x00"
|
441
|
+
@screen_type = read_int32
|
442
|
+
@input = read_int32
|
443
|
+
@screen_input = read_int32
|
444
|
+
@version = read_int32
|
445
|
+
@screen_config = read_int32
|
446
|
+
end
|
447
|
+
def inspect
|
448
|
+
"<ResTableConfig size:#{@size}, imei:#{@imei}, la:'#{@locale_lang}' cn:'#{@locale_contry}'"
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
class ResTableTypeSpec < ChunkHeader
|
453
|
+
attr_reader :id, :entry_count
|
454
|
+
|
455
|
+
def parse
|
456
|
+
super
|
457
|
+
@id = read_int8
|
458
|
+
res0 = read_int8 # must be 0.(maybe 4byte align)
|
459
|
+
res1 = read_int16 # must be 0.(maybe 4byte align)
|
460
|
+
@entry_count = read_int32
|
461
|
+
end
|
462
|
+
private :parse
|
463
|
+
|
464
|
+
def inspect
|
465
|
+
"<ResTableTypeSpec id:#{@id} entry count:#{@entry_count}>"
|
466
|
+
end
|
467
|
+
end
|
468
|
+
class ResTableEntry < Chunk
|
469
|
+
NO_ENTRY = 0xFFFFFFFF
|
470
|
+
|
471
|
+
# @return [ResTableEntry] if not set FLAG_COMPLEX
|
472
|
+
# @return [ResTableMapEntry] if not set FLAG_COMPLEX
|
473
|
+
def self.read_entry(data, offset)
|
474
|
+
flag = data[offset + 2, 2].unpack('v')[0]
|
475
|
+
if flag & ResTableEntry::FLAG_COMPLEX == 0
|
476
|
+
ResTableEntry.new(data, offset)
|
477
|
+
else
|
478
|
+
ResTableMapEntry.new(data, offset)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# If set, this is a complex entry, holding a set of name/value
|
483
|
+
# mappings. It is followed by an array of ResTable_map structures.
|
484
|
+
FLAG_COMPLEX = 0x01
|
485
|
+
# If set, this resource has been declared public, so libraries
|
486
|
+
# are allowed to reference it.
|
487
|
+
FLAG_PUBLIC = 0x02
|
488
|
+
|
489
|
+
attr_reader :size, :key, :val
|
490
|
+
def parse
|
491
|
+
@size = read_int16
|
492
|
+
@flag = read_int16
|
493
|
+
@key = read_int32 # RefStringPool_key
|
494
|
+
@val = ResValue.new(@data, current_position)
|
495
|
+
end
|
496
|
+
private :parse
|
497
|
+
|
498
|
+
def inspect
|
499
|
+
"<ResTableEntry @size=#{@size}, @key=#{@key} @flag=#{@flag}>"
|
500
|
+
end
|
501
|
+
end
|
502
|
+
class ResTableMapEntry < ResTableEntry
|
503
|
+
attr_reader :parent, :count
|
504
|
+
def parse
|
505
|
+
super
|
506
|
+
# resource identifier of the parent mapping, 0 if there is none.
|
507
|
+
@parent = read_int32
|
508
|
+
# number of name/value pairs that follw for FLAG_COMPLEX
|
509
|
+
@count = read_int32
|
510
|
+
# TODO: implement read ResTableMap object
|
511
|
+
end
|
512
|
+
private :parse
|
513
|
+
end
|
514
|
+
class ResTableMap < Chunk
|
515
|
+
def size
|
516
|
+
@val.size + 4
|
517
|
+
end
|
518
|
+
def parse
|
519
|
+
@name = read_int32
|
520
|
+
@val = ResValue.new(@data, current_position)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
class ResValue < Chunk
|
525
|
+
attr_reader :size, :data_type, :data
|
526
|
+
def parse
|
527
|
+
@size = read_int16
|
528
|
+
res0 = read_int8 # Always set 0.
|
529
|
+
@data_type = read_int8
|
530
|
+
@data = read_int32
|
531
|
+
end
|
532
|
+
private :parse
|
533
|
+
end
|
534
|
+
|
535
|
+
######################################################################
|
536
|
+
# @returns [Hash] { name(String) => value(ResTablePackage) }
|
537
|
+
attr_reader :packages
|
538
|
+
|
539
|
+
def initialize(data)
|
540
|
+
data.force_encoding(Encoding::ASCII_8BIT)
|
541
|
+
@data = data
|
542
|
+
parse()
|
543
|
+
end
|
544
|
+
|
545
|
+
|
546
|
+
# @return [Array<String>] all strings defined in arsc.
|
547
|
+
def strings
|
548
|
+
@string_pool.strings
|
549
|
+
end
|
550
|
+
|
551
|
+
# @return [Fixnum] number of packages
|
552
|
+
def package_count
|
553
|
+
@res_table.package_count
|
554
|
+
end
|
555
|
+
|
556
|
+
# This method only support string resource for now.
|
557
|
+
# find resource by resource id
|
558
|
+
# @param [String] res_id (like '@0x7f010001' or '@string/key')
|
559
|
+
# @param [Hash] opts option
|
560
|
+
# @option opts [String] :lang language code like 'ja', 'cn'...
|
561
|
+
# @option opts [String] :contry cantry code like 'jp'...
|
562
|
+
# @raise [ArgumentError] invalid id format
|
563
|
+
# @note
|
564
|
+
# This method only support string resource for now.
|
565
|
+
# @note
|
566
|
+
# Always return nil if assign not string type res id.
|
567
|
+
# @since 0.5.0
|
568
|
+
def find(rsc_id, opt={})
|
569
|
+
first_pkg.find(rsc_id, opt)
|
570
|
+
end
|
571
|
+
|
572
|
+
# @param [String] hex_id hexoctet format resource id('@0x7f010001')
|
573
|
+
# @return [String] readable resource id ('@string/key')
|
574
|
+
# @since 0.5.0
|
575
|
+
def res_readable_id(hex_id)
|
576
|
+
first_pkg.res_readable_id(hex_id)
|
577
|
+
end
|
578
|
+
|
579
|
+
# convert readable resource id to hex id
|
580
|
+
# @param [String] readable_id readable resource id ('@string/key')
|
581
|
+
# @return [String] hexoctet format resource id('@0x7f010001')
|
582
|
+
# @since 0.5.0
|
583
|
+
def res_hex_id(readable_id)
|
584
|
+
first_pkg.res_hex_id(readable_id)
|
585
|
+
end
|
586
|
+
|
587
|
+
def first_pkg
|
588
|
+
@packages.first[1]
|
589
|
+
end
|
590
|
+
|
591
|
+
private
|
592
|
+
|
593
|
+
def parse
|
594
|
+
offset = 0
|
595
|
+
|
596
|
+
while offset < @data.size
|
597
|
+
type = @data[offset, 2].unpack('v')[0]
|
598
|
+
#print "[%#08x] " % offset
|
599
|
+
@packages = {}
|
600
|
+
case type
|
601
|
+
when 0x0001 # RES_STRING_POOL_TYPE
|
602
|
+
@string_pool = ResStringPool.new(@data, offset)
|
603
|
+
offset += @string_pool.size
|
604
|
+
#puts "RES_STRING_POOL_TYPE %#x, %#x" % [@string_pool.size, offset]
|
605
|
+
when 0x0002 # RES_TABLE_TYPE
|
606
|
+
#puts "RES_TABLE_TYPE"
|
607
|
+
@res_table = ResTableHeader.new(@data, offset)
|
608
|
+
offset += @res_table.header_size
|
609
|
+
when 0x0200 # RES_TABLE_PACKAGE_TYPE
|
610
|
+
#puts "RES_TABLE_PACKAGE_TYPE"
|
611
|
+
pkg = ResTablePackage.new(@data, offset)
|
612
|
+
pkg.global_string_pool = @string_pool
|
613
|
+
offset += pkg.size
|
614
|
+
@packages[pkg.name] = pkg
|
615
|
+
else
|
616
|
+
raise "chunk type error: type:%#04x" % type
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
end
|