ruby_android 0.0.2 → 0.7.2

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/.name +1 -0
  3. data/.idea/.rakeTasks +7 -0
  4. data/.idea/encodings.xml +5 -0
  5. data/.idea/misc.xml +5 -0
  6. data/.idea/modules.xml +9 -0
  7. data/.idea/ruby_apk.iml +51 -0
  8. data/.idea/scopes/scope_settings.xml +5 -0
  9. data/.idea/vcs.xml +7 -0
  10. data/.idea/workspace.xml +508 -0
  11. data/.travis.yml +4 -0
  12. data/CHANGELOG.md +51 -0
  13. data/Gemfile +3 -3
  14. data/Gemfile.lock +73 -0
  15. data/LICENSE.txt +2 -2
  16. data/Rakefile +42 -1
  17. data/VERSION +1 -0
  18. data/lib/android/apk.rb +207 -0
  19. data/lib/android/axml_parser.rb +173 -0
  20. data/lib/android/dex/access_flag.rb +74 -0
  21. data/lib/android/dex/dex_object.rb +475 -0
  22. data/lib/android/dex/info.rb +151 -0
  23. data/lib/android/dex/utils.rb +45 -0
  24. data/lib/android/dex.rb +92 -0
  25. data/lib/android/layout.rb +44 -0
  26. data/lib/android/manifest.rb +249 -0
  27. data/lib/android/resource.rb +529 -0
  28. data/lib/android/utils.rb +55 -0
  29. data/lib/ruby_apk.rb +7 -0
  30. data/ruby_android.gemspec +103 -17
  31. data/spec/apk_spec.rb +301 -0
  32. data/spec/axml_parser_spec.rb +67 -0
  33. data/spec/data/sample.apk +0 -0
  34. data/spec/data/sample_AndroidManifest.xml +0 -0
  35. data/spec/data/sample_classes.dex +0 -0
  36. data/spec/data/sample_resources.arsc +0 -0
  37. data/spec/data/sample_resources_utf16.arsc +0 -0
  38. data/spec/data/str_resources.arsc +0 -0
  39. data/spec/dex/access_flag_spec.rb +42 -0
  40. data/spec/dex/dex_object_spec.rb +118 -0
  41. data/spec/dex/info_spec.rb +121 -0
  42. data/spec/dex/utils_spec.rb +56 -0
  43. data/spec/dex_spec.rb +59 -0
  44. data/spec/layout_spec.rb +27 -0
  45. data/spec/manifest_spec.rb +221 -0
  46. data/spec/resource_spec.rb +170 -0
  47. data/spec/ruby_apk_spec.rb +4 -0
  48. data/spec/spec_helper.rb +17 -0
  49. data/spec/utils_spec.rb +90 -0
  50. metadata +112 -27
  51. data/.gitignore +0 -14
  52. data/lib/ruby_android/version.rb +0 -3
  53. data/lib/ruby_android.rb +0 -7
@@ -0,0 +1,529 @@
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 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'
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_lang = {}
259
+ @res_strings_contry = {}
260
+ begin
261
+ type = type_id('string')
262
+ rescue NotFoundError
263
+ return
264
+ end
265
+ @types[type_id('string')].each do |type|
266
+ str_hash = {}
267
+ type.entry_count.times do |i|
268
+ entry = type[i]
269
+ if entry.nil?
270
+ str_hash[i] = nil
271
+ else
272
+ str_hash[i] = @global_string_pool.strings[type[i].val.data]
273
+ end
274
+ end
275
+ lang = type.config.locale_lang
276
+ contry = type.config.locale_contry
277
+ if lang.nil? && contry.nil?
278
+ @res_strings_default = str_hash
279
+ else
280
+ @res_strings_lang[lang] = str_hash unless lang.nil?
281
+ @res_strings_contry[contry] = str_hash unless contry.nil?
282
+ end
283
+ end
284
+ end
285
+ private :extract_res_strings
286
+
287
+ def inspect
288
+ "<ResTablePackage offset:%#08x, size:%#x, name:\"%s\">" % [@offset, @size, @name]
289
+ end
290
+ end
291
+
292
+ class ResTableType < ChunkHeader
293
+ attr_reader :id, :entry_count, :entry_start, :config
294
+ attr_reader :keys
295
+
296
+ def initialize(data, offset, pkg)
297
+ @pkg = pkg
298
+ super(data, offset)
299
+ end
300
+ # @param [String] index key name
301
+ # @param [Fixnum] index key index
302
+ # @return [ResTableEntry]
303
+ # @return [ResTableMapEntry]
304
+ # @return nil if entry index is NO_ENTRY(0xFFFFFFFF)
305
+ def [](index)
306
+ @entries[index]
307
+ end
308
+
309
+ def parse
310
+ super
311
+ @id = read_int8
312
+ res0 = read_int8 # must be 0.(maybe 4byte align)
313
+ res1 = read_int16 # must be 0.(maybe 4byte align)
314
+ @entry_count = read_int32
315
+ @entry_start = read_int32
316
+ @config = ResTableConfig.new(@data, current_position)
317
+ @data_io.seek(@config.size, IO::SEEK_CUR)
318
+
319
+ @entries = []
320
+ @keys = {}
321
+ @entry_count.times do |i|
322
+ entry_index = read_int32
323
+ if entry_index == ResTableEntry::NO_ENTRY
324
+ @entries << nil
325
+ else
326
+ entry = ResTableEntry.read_entry(@data, @offset + @entry_start + entry_index)
327
+ @entries << entry
328
+ @keys[@pkg.key(entry.key)] = i
329
+ end
330
+ end
331
+ end
332
+ private :parse
333
+
334
+
335
+ def inspect
336
+ "<ResTableType offset:0x#{@offset.to_s(16)}, id:#{@id}, " +
337
+ "count:#{@entry_count}, start:0x#{@entry_start.to_s(16)}>"
338
+ end
339
+ end
340
+
341
+ class ResTableConfig < Chunk
342
+ attr_reader :size, :imei, :locale_lang, :locale_contry, :input
343
+ attr_reader :screen_input, :version, :screen_config
344
+ def parse
345
+ @size = read_int32
346
+ @imei = read_int32
347
+ la = @data_io.read(2)
348
+ @locale_lang = la unless la == "\x00\x00"
349
+ cn = @data_io.read(2)
350
+ @locale_contry = cn unless cn == "\x00\x00"
351
+ @screen_type = read_int32
352
+ @input = read_int32
353
+ @screen_input = read_int32
354
+ @version = read_int32
355
+ @screen_config = read_int32
356
+ end
357
+ def inspect
358
+ "<ResTableConfig size:#{@size}, imei:#{@imei}, la:'#{@locale_lang}' cn:'#{@locale_contry}'"
359
+ end
360
+ end
361
+
362
+ class ResTableTypeSpec < ChunkHeader
363
+ attr_reader :id, :entry_count
364
+
365
+ def parse
366
+ super
367
+ @id = read_int8
368
+ res0 = read_int8 # must be 0.(maybe 4byte align)
369
+ res1 = read_int16 # must be 0.(maybe 4byte align)
370
+ @entry_count = read_int32
371
+ end
372
+ private :parse
373
+
374
+ def inspect
375
+ "<ResTableTypeSpec id:#{@id} entry count:#{@entry_count}>"
376
+ end
377
+ end
378
+ class ResTableEntry < Chunk
379
+ NO_ENTRY = 0xFFFFFFFF
380
+
381
+ # @return [ResTableEntry] if not set FLAG_COMPLEX
382
+ # @return [ResTableMapEntry] if not set FLAG_COMPLEX
383
+ def self.read_entry(data, offset)
384
+ flag = data[offset + 2, 2].unpack('v')[0]
385
+ if flag & ResTableEntry::FLAG_COMPLEX == 0
386
+ ResTableEntry.new(data, offset)
387
+ else
388
+ ResTableMapEntry.new(data, offset)
389
+ end
390
+ end
391
+
392
+ # If set, this is a complex entry, holding a set of name/value
393
+ # mappings. It is followed by an array of ResTable_map structures.
394
+ FLAG_COMPLEX = 0x01
395
+ # If set, this resource has been declared public, so libraries
396
+ # are allowed to reference it.
397
+ FLAG_PUBLIC = 0x02
398
+
399
+ attr_reader :size, :key, :val
400
+ def parse
401
+ @size = read_int16
402
+ @flag = read_int16
403
+ @key = read_int32 # RefStringPool_key
404
+ @val = ResValue.new(@data, current_position)
405
+ end
406
+ private :parse
407
+
408
+ def inspect
409
+ "<ResTableEntry @size=#{@size}, @key=#{@key} @flag=#{@flag}>"
410
+ end
411
+ end
412
+ class ResTableMapEntry < ResTableEntry
413
+ attr_reader :parent, :count
414
+ def parse
415
+ super
416
+ # resource identifier of the parent mapping, 0 if there is none.
417
+ @parent = read_int32
418
+ # number of name/value pairs that follw for FLAG_COMPLEX
419
+ @count = read_int32
420
+ # TODO: implement read ResTableMap object
421
+ end
422
+ private :parse
423
+ end
424
+ class ResTableMap < Chunk
425
+ def size
426
+ @val.size + 4
427
+ end
428
+ def parse
429
+ @name = read_int32
430
+ @val = ResValue.new(@data, current_position)
431
+ end
432
+ end
433
+
434
+ class ResValue < Chunk
435
+ attr_reader :size, :data_type, :data
436
+ def parse
437
+ @size = read_int16
438
+ res0 = read_int8 # Always set 0.
439
+ @data_type = read_int8
440
+ @data = read_int32
441
+ end
442
+ private :parse
443
+ end
444
+
445
+ ######################################################################
446
+ # @returns [Hash] { name(String) => value(ResTablePackage) }
447
+ attr_reader :packages
448
+
449
+ def initialize(data)
450
+ data.force_encoding(Encoding::ASCII_8BIT)
451
+ @data = data
452
+ parse()
453
+ end
454
+
455
+
456
+ # @return [Array<String>] all strings defined in arsc.
457
+ def strings
458
+ @string_pool.strings
459
+ end
460
+
461
+ # @return [Fixnum] number of packages
462
+ def package_count
463
+ @res_table.package_count
464
+ end
465
+
466
+ # This method only support string resource for now.
467
+ # find resource by resource id
468
+ # @param [String] res_id (like '@0x7f010001' or '@string/key')
469
+ # @param [Hash] opts option
470
+ # @option opts [String] :lang language code like 'ja', 'cn'...
471
+ # @option opts [String] :contry cantry code like 'jp'...
472
+ # @raise [ArgumentError] invalid id format
473
+ # @note
474
+ # This method only support string resource for now.
475
+ # @note
476
+ # Always return nil if assign not string type res id.
477
+ # @since 0.5.0
478
+ def find(rsc_id, opt={})
479
+ first_pkg.find(rsc_id, opt)
480
+ end
481
+
482
+ # @param [String] hex_id hexoctet format resource id('@0x7f010001')
483
+ # @return [String] readable resource id ('@string/key')
484
+ # @since 0.5.0
485
+ def res_readable_id(hex_id)
486
+ first_pkg.res_readable_id(hex_id)
487
+ end
488
+
489
+ # convert readable resource id to hex id
490
+ # @param [String] readable_id readable resource id ('@string/key')
491
+ # @return [String] hexoctet format resource id('@0x7f010001')
492
+ # @since 0.5.0
493
+ def res_hex_id(readable_id)
494
+ first_pkg.res_hex_id(readable_id)
495
+ end
496
+
497
+ def first_pkg
498
+ @packages.first[1]
499
+ end
500
+ private
501
+ def parse
502
+ offset = 0
503
+
504
+ while offset < @data.size
505
+ type = @data[offset, 2].unpack('v')[0]
506
+ #print "[%#08x] " % offset
507
+ @packages = {}
508
+ case type
509
+ when 0x0001 # RES_STRING_POOL_TYPE
510
+ @string_pool = ResStringPool.new(@data, offset)
511
+ offset += @string_pool.size
512
+ #puts "RES_STRING_POOL_TYPE %#x, %#x" % [@string_pool.size, offset]
513
+ when 0x0002 # RES_TABLE_TYPE
514
+ #puts "RES_TABLE_TYPE"
515
+ @res_table = ResTableHeader.new(@data, offset)
516
+ offset += @res_table.header_size
517
+ when 0x0200 # RES_TABLE_PACKAGE_TYPE
518
+ #puts "RES_TABLE_PACKAGE_TYPE"
519
+ pkg = ResTablePackage.new(@data, offset)
520
+ pkg.global_string_pool = @string_pool
521
+ offset += pkg.size
522
+ @packages[pkg.name] = pkg
523
+ else
524
+ raise "chunk type error: type:%#04x" % type
525
+ end
526
+ end
527
+ end
528
+ end
529
+ end
@@ -0,0 +1,55 @@
1
+
2
+ module Android
3
+ # Utility methods
4
+ module Utils
5
+ # path is apk file or not.
6
+ # @param [String] path target file path
7
+ # @return [Boolean]
8
+ def self.apk?(path)
9
+ begin
10
+ apk = Apk.new(path)
11
+ return true
12
+ rescue => e
13
+ return false
14
+ end
15
+ end
16
+
17
+ # data is elf file or not.
18
+ # @param [String] data target data
19
+ # @return [Boolean]
20
+ def self.elf?(data)
21
+ data[0..3] == "\x7f\x45\x4c\x46"
22
+ rescue => e
23
+ false
24
+ end
25
+
26
+ # data is cert file or not.
27
+ # @param [String] data target data
28
+ # @return [Boolean]
29
+ def self.cert?(data)
30
+ data[0..1] == "\x30\x82"
31
+ rescue => e
32
+ false
33
+ end
34
+
35
+ # data is dex file or not.
36
+ # @param [String] data target data
37
+ # @return [Boolean]
38
+ def self.dex?(data)
39
+ data[0..7] == "\x64\x65\x78\x0a\x30\x33\x35\x00" # "dex\n035\0"
40
+ rescue => e
41
+ false
42
+ end
43
+
44
+ # data is valid dex file or not.
45
+ # @param [String] data target data
46
+ # @return [Boolean]
47
+ def self.valid_dex?(data)
48
+ Android::Dex.new(data)
49
+ true
50
+ rescue => e
51
+ false
52
+ end
53
+ end
54
+ end
55
+
data/lib/ruby_apk.rb ADDED
@@ -0,0 +1,7 @@
1
+ require_relative 'android/apk'
2
+ require_relative 'android/manifest'
3
+ require_relative 'android/axml_parser'
4
+ require_relative 'android/dex'
5
+ require_relative 'android/resource'
6
+ require_relative 'android/utils'
7
+ require_relative 'android/layout'