ruby_android_apk 0.7.7.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.
@@ -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