android_parser 2.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|