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