ruby_android_apk 0.7.7.1

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