ruby_android 0.0.2 → 0.7.2

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