android_parser 2.4.1

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