ruby_apk 0.4.2 → 0.5.0
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.
- data/CHANGELOG.md +3 -0
- data/README.md +17 -1
- data/VERSION +1 -1
- data/lib/android/resource.rb +398 -15
- data/ruby_apk.gemspec +4 -3
- data/spec/data/str_resources.arsc +0 -0
- data/spec/resource_spec.rb +69 -0
- metadata +5 -4
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -5,7 +5,7 @@ Android Apk static analysis library for Ruby.
|
|
5
5
|
- ruby(>=1.9.x)
|
6
6
|
|
7
7
|
## Install
|
8
|
-
|
8
|
+
$ gem install ruby_apk
|
9
9
|
|
10
10
|
## Usage
|
11
11
|
### Initialize
|
@@ -72,6 +72,22 @@ end
|
|
72
72
|
rsc = Android::Resource.new(rsc_data)
|
73
73
|
```
|
74
74
|
|
75
|
+
### Resolve resource id
|
76
|
+
This feature supports only srting resources for now.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
apk = Android::Apk.new('sample.apk')
|
80
|
+
# assigns readable resource id
|
81
|
+
puts rsc.find('@string/app_name') # => 'application name'
|
82
|
+
|
83
|
+
# assigns hex resource id
|
84
|
+
puts rsc.find('@0x7f040000') # => 'application name'
|
85
|
+
|
86
|
+
# you can set lang attribute.
|
87
|
+
puts rsc.find('@0x7f040000', :lang => 'ja')
|
88
|
+
```
|
89
|
+
|
90
|
+
|
75
91
|
### Dex
|
76
92
|
#### Extract dex information
|
77
93
|
```ruby
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/lib/android/resource.rb
CHANGED
@@ -1,26 +1,22 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'stringio'
|
3
|
-
require 'csv'
|
4
|
-
require 'ruby_apk'
|
5
3
|
|
6
4
|
module Android
|
7
5
|
# based on Android OS source code
|
8
6
|
# /frameworks/base/include/utils/ResourceTypes.h
|
7
|
+
# @see http://justanapplication.wordpress.com/category/android/android-resources/
|
9
8
|
class Resource
|
10
|
-
class
|
11
|
-
attr_reader :type, :header_size, :size
|
9
|
+
class Chunk
|
12
10
|
def initialize(data, offset)
|
13
11
|
@data = data
|
14
12
|
@offset = offset
|
13
|
+
exec_parse
|
14
|
+
end
|
15
|
+
def exec_parse
|
15
16
|
@data_io = StringIO.new(@data, 'rb')
|
16
|
-
@data_io.seek(offset)
|
17
|
+
@data_io.seek(@offset)
|
17
18
|
parse
|
18
|
-
|
19
|
-
private
|
20
|
-
def parse
|
21
|
-
@type = read_int16
|
22
|
-
@header_size = read_int16
|
23
|
-
@size = read_int32
|
19
|
+
@data_io.close
|
24
20
|
end
|
25
21
|
def read_int32
|
26
22
|
@data_io.read(4).unpack('V')[0]
|
@@ -28,6 +24,22 @@ module Android
|
|
28
24
|
def read_int16
|
29
25
|
@data_io.read(2).unpack('v')[0]
|
30
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
|
31
43
|
end
|
32
44
|
|
33
45
|
class ResTableHeader < ChunkHeader
|
@@ -37,6 +49,7 @@ module Android
|
|
37
49
|
@package_count = read_int32
|
38
50
|
end
|
39
51
|
end
|
52
|
+
|
40
53
|
class ResStringPool < ChunkHeader
|
41
54
|
SORTED_FLAG = 1 << 0
|
42
55
|
UTF8_FLAG = 1 << 8
|
@@ -95,37 +108,407 @@ module Android
|
|
95
108
|
end
|
96
109
|
end
|
97
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 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
|
+
if type(tid) == 'string'
|
136
|
+
return find_res_string(key, opts)
|
137
|
+
else
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def find_res_string(key, opts={})
|
143
|
+
unless opts[:lang].nil?
|
144
|
+
string = @res_strings_lang[opts[:lang]]
|
145
|
+
end
|
146
|
+
unless opts[:contry].nil?
|
147
|
+
string = @res_strings_contry[opts[:contry]]
|
148
|
+
end
|
149
|
+
string = @res_strings_default if string.nil?
|
150
|
+
raise NotFoundError unless string.has_key? key
|
151
|
+
return string[key]
|
152
|
+
end
|
153
|
+
private :find_res_string
|
154
|
+
|
155
|
+
# convert string resource id to fixnum
|
156
|
+
# @param [String] res_id (like '@0x7f010001' or '@string/key')
|
157
|
+
# @return [Fixnum] integer id (like 0x7f010001)
|
158
|
+
# @raise [ArgumentError] invalid format
|
159
|
+
def strid2int(res_id)
|
160
|
+
case res_id
|
161
|
+
when /^@?0x[0-9a-fA-F]{8}$/
|
162
|
+
return res_id.sub(/^@/,'').to_i(16)
|
163
|
+
when /^@?\w+\/\w+/
|
164
|
+
return res_hex_id(res_id).sub(/^@/,'').to_i(16)
|
165
|
+
else
|
166
|
+
raise ArgumentError
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def res_readable_id(hex_id)
|
171
|
+
if hex_id.kind_of? String
|
172
|
+
hex_id = hex_id.sub(/^@/,'').to_i(16)
|
173
|
+
end
|
174
|
+
tid = ((hex_id&0xff0000) >>16)
|
175
|
+
key = hex_id&0xffff
|
176
|
+
raise NotFoundError if !@types.has_key?(tid) || @types[tid][0][key].nil?
|
177
|
+
keyid= @types[tid][0][key].key # ugh!
|
178
|
+
"@#{type(tid)}/#{key(keyid)}"
|
179
|
+
end
|
180
|
+
def res_hex_id(readable_id, opt={})
|
181
|
+
dummy, typestr, keystr = readable_id.match(/^@?(\w+)\/(\w+)$/).to_a
|
182
|
+
tid = type_id(typestr)
|
183
|
+
raise NotFoundError unless @types.has_key?(tid)
|
184
|
+
keyid = @types[tid][0].keys[keystr]
|
185
|
+
raise NotFoundError if keyid.nil?
|
186
|
+
"@0x7f%02x%04x" % [tid, keyid]
|
187
|
+
end
|
188
|
+
|
189
|
+
def type_strings
|
190
|
+
@type_strings.strings
|
191
|
+
end
|
192
|
+
def type(id)
|
193
|
+
type_strings[id-1]
|
194
|
+
end
|
195
|
+
def type_id(str)
|
196
|
+
raise NotFoundError unless type_strings.include? str
|
197
|
+
type_strings.index(str) + 1
|
198
|
+
end
|
199
|
+
def key_strings
|
200
|
+
@key_strings.strings
|
201
|
+
end
|
202
|
+
def key(id)
|
203
|
+
key_strings[id]
|
204
|
+
end
|
205
|
+
def key_id(str)
|
206
|
+
raise NotFoundError unless key_strings.include? str
|
207
|
+
key_strings.index(str)
|
208
|
+
end
|
209
|
+
|
210
|
+
def parse
|
211
|
+
super
|
212
|
+
@id = read_int32
|
213
|
+
@name = @data_io.read(256).force_encoding(Encoding::UTF_16LE)
|
214
|
+
@name.encode!(Encoding::UTF_8).strip!
|
215
|
+
type_strings_offset = read_int32
|
216
|
+
@type_strings = ResStringPool.new(@data, @offset + type_strings_offset)
|
217
|
+
@last_public_type = read_int32
|
218
|
+
key_strings_offset = read_int32
|
219
|
+
@key_strings = ResStringPool.new(@data, @offset + key_strings_offset)
|
220
|
+
@last_public_key = read_int32
|
221
|
+
|
222
|
+
offset = @offset + key_strings_offset + @key_strings.size
|
223
|
+
|
224
|
+
@types = {}
|
225
|
+
@specs = {}
|
226
|
+
while offset < (@offset + @size)
|
227
|
+
type = @data[offset, 2].unpack('v')[0]
|
228
|
+
case type
|
229
|
+
when 0x0201 # RES_TABLE_TYPE_TYPE
|
230
|
+
type = ResTableType.new(@data, offset, self)
|
231
|
+
offset += type.size
|
232
|
+
@types[type.id] = [] if @types[type.id].nil?
|
233
|
+
@types[type.id] << type
|
234
|
+
when 0x0202 # RES_TABLE_TYPE_SPEC_TYPE`
|
235
|
+
spec = ResTableTypeSpec.new(@data, offset)
|
236
|
+
offset += spec.size
|
237
|
+
@specs[spec.id] = [] if @specs[spec.id].nil?
|
238
|
+
@specs[spec.id] << spec
|
239
|
+
else
|
240
|
+
raise "chunk type error: type:%#04x" % type
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
private :parse
|
245
|
+
|
246
|
+
def extract_res_strings
|
247
|
+
@res_strings_lang = {}
|
248
|
+
@res_strings_contry = {}
|
249
|
+
begin
|
250
|
+
type = type_id('string')
|
251
|
+
rescue NotFoundError
|
252
|
+
return
|
253
|
+
end
|
254
|
+
@types[type_id('string')].each do |type|
|
255
|
+
str_hash = {}
|
256
|
+
type.entry_count.times do |i|
|
257
|
+
entry = type[i]
|
258
|
+
if entry.nil?
|
259
|
+
str_hash[i] = nil
|
260
|
+
else
|
261
|
+
str_hash[i] = @global_string_pool.strings[type[i].val.data]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
lang = type.config.locale_lang
|
265
|
+
contry = type.config.locale_contry
|
266
|
+
if lang.nil? && contry.nil?
|
267
|
+
@res_strings_default = str_hash
|
268
|
+
else
|
269
|
+
@res_strings_lang[lang] = str_hash unless lang.nil?
|
270
|
+
@res_strings_contry[contry] = str_hash unless contry.nil?
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
private :extract_res_strings
|
275
|
+
|
276
|
+
def inspect
|
277
|
+
"<ResTablePackage offset:%#08x, size:%#x, name:\"%s\">" % [@offset, @size, @name]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
class ResTableType < ChunkHeader
|
282
|
+
attr_reader :id, :entry_count, :entry_start, :config
|
283
|
+
attr_reader :keys
|
284
|
+
|
285
|
+
def initialize(data, offset, pkg)
|
286
|
+
@pkg = pkg
|
287
|
+
super(data, offset)
|
288
|
+
end
|
289
|
+
# @param [String] index key name
|
290
|
+
# @param [Fixnum] index key index
|
291
|
+
# @return [ResTableEntry]
|
292
|
+
# @return [ResTableMapEntry]
|
293
|
+
# @return nil if entry index is NO_ENTRY(0xFFFFFFFF)
|
294
|
+
def [](index)
|
295
|
+
@entries[index]
|
296
|
+
end
|
297
|
+
|
298
|
+
def parse
|
299
|
+
super
|
300
|
+
@id = read_int8
|
301
|
+
res0 = read_int8 # must be 0.(maybe 4byte align)
|
302
|
+
res1 = read_int16 # must be 0.(maybe 4byte align)
|
303
|
+
@entry_count = read_int32
|
304
|
+
@entry_start = read_int32
|
305
|
+
@config = ResTableConfig.new(@data, current_position)
|
306
|
+
@data_io.seek(@config.size, IO::SEEK_CUR)
|
307
|
+
|
308
|
+
@entries = []
|
309
|
+
@keys = {}
|
310
|
+
@entry_count.times do |i|
|
311
|
+
entry_index = read_int32
|
312
|
+
if entry_index == ResTableEntry::NO_ENTRY
|
313
|
+
@entries << nil
|
314
|
+
else
|
315
|
+
entry = ResTableEntry.read_entry(@data, @offset + @entry_start + entry_index)
|
316
|
+
@entries << entry
|
317
|
+
@keys[@pkg.key(entry.key)] = i
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
private :parse
|
322
|
+
|
323
|
+
|
324
|
+
def inspect
|
325
|
+
"<ResTableType offset:0x#{@offset.to_s(16)}, id:#{@id}, " +
|
326
|
+
"count:#{@entry_count}, start:0x#{@entry_start.to_s(16)}>"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
class ResTableConfig < Chunk
|
331
|
+
attr_reader :size, :imei, :locale_lang, :locale_contry, :input
|
332
|
+
attr_reader :screen_input, :version, :screen_config
|
333
|
+
def parse
|
334
|
+
@size = read_int32
|
335
|
+
@imei = read_int32
|
336
|
+
la = @data_io.read(2)
|
337
|
+
@locale_lang = la unless la == "\x00\x00"
|
338
|
+
cn = @data_io.read(2)
|
339
|
+
@locale_contry = cn unless cn == "\x00\x00"
|
340
|
+
@screen_type = read_int32
|
341
|
+
@input = read_int32
|
342
|
+
@screen_input = read_int32
|
343
|
+
@version = read_int32
|
344
|
+
@screen_config = read_int32
|
345
|
+
end
|
346
|
+
def inspect
|
347
|
+
"<ResTableConfig size:#{@size}, imei:#{@imei}, la:'#{@locale_lang}' cn:'#{@locale_contry}'"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
class ResTableTypeSpec < ChunkHeader
|
352
|
+
attr_reader :id, :entry_count
|
353
|
+
|
354
|
+
def parse
|
355
|
+
super
|
356
|
+
@id = read_int8
|
357
|
+
res0 = read_int8 # must be 0.(maybe 4byte align)
|
358
|
+
res1 = read_int16 # must be 0.(maybe 4byte align)
|
359
|
+
@entry_count = read_int32
|
360
|
+
end
|
361
|
+
private :parse
|
362
|
+
|
363
|
+
def inspect
|
364
|
+
"<ResTableTypeSpec id:#{@id} entry count:#{@entry_count}>"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
class ResTableEntry < Chunk
|
368
|
+
NO_ENTRY = 0xFFFFFFFF
|
369
|
+
|
370
|
+
# @return [ResTableEntry] if not set FLAG_COMPLEX
|
371
|
+
# @return [ResTableMapEntry] if not set FLAG_COMPLEX
|
372
|
+
def self.read_entry(data, offset)
|
373
|
+
flag = data[offset + 2, 2].unpack('v')[0]
|
374
|
+
if flag & ResTableEntry::FLAG_COMPLEX == 0
|
375
|
+
ResTableEntry.new(data, offset)
|
376
|
+
else
|
377
|
+
ResTableMapEntry.new(data, offset)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# If set, this is a complex entry, holding a set of name/value
|
382
|
+
# mappings. It is followed by an array of ResTable_map structures.
|
383
|
+
FLAG_COMPLEX = 0x01
|
384
|
+
# If set, this resource has been declared public, so libraries
|
385
|
+
# are allowed to reference it.
|
386
|
+
FLAG_PUBLIC = 0x02
|
387
|
+
|
388
|
+
attr_reader :size, :key, :val
|
389
|
+
def parse
|
390
|
+
@size = read_int16
|
391
|
+
@flag = read_int16
|
392
|
+
@key = read_int32 # RefStringPool_key
|
393
|
+
@val = ResValue.new(@data, current_position)
|
394
|
+
end
|
395
|
+
private :parse
|
396
|
+
|
397
|
+
def inspect
|
398
|
+
"<ResTableEntry @size=#{@size}, @key=#{@key} @flag=#{@flag}>"
|
399
|
+
end
|
400
|
+
end
|
401
|
+
class ResTableMapEntry < ResTableEntry
|
402
|
+
attr_reader :parent, :count
|
403
|
+
def parse
|
404
|
+
super
|
405
|
+
# resource identifier of the parent mapping, 0 if there is none.
|
406
|
+
@parent = read_int32
|
407
|
+
# number of name/value pairs that follw for FLAG_COMPLEX
|
408
|
+
@count = read_int32
|
409
|
+
# TODO: implement read ResTableMap object
|
410
|
+
end
|
411
|
+
private :parse
|
412
|
+
end
|
413
|
+
class ResTableMap < Chunk
|
414
|
+
def size
|
415
|
+
@val.size + 4
|
416
|
+
end
|
417
|
+
def parse
|
418
|
+
@name = read_int32
|
419
|
+
@val = ResValue.new(@data, current_position)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
class ResValue < Chunk
|
424
|
+
attr_reader :size, :data_type, :data
|
425
|
+
def parse
|
426
|
+
@size = read_int16
|
427
|
+
res0 = read_int8 # Always set 0.
|
428
|
+
@data_type = read_int8
|
429
|
+
@data = read_int32
|
430
|
+
end
|
431
|
+
private :parse
|
432
|
+
end
|
433
|
+
|
98
434
|
######################################################################
|
435
|
+
# @returns [Hash] { name(String) => value(ResTablePackage) }
|
436
|
+
attr_reader :packages
|
437
|
+
|
99
438
|
def initialize(data)
|
100
439
|
data.force_encoding(Encoding::ASCII_8BIT)
|
101
440
|
@data = data
|
102
441
|
parse()
|
103
442
|
end
|
104
443
|
|
444
|
+
|
445
|
+
# @return [Array<String>] all strings defined in arsc.
|
105
446
|
def strings
|
106
447
|
@string_pool.strings
|
107
448
|
end
|
449
|
+
|
450
|
+
# @return [Fixnum] number of packages
|
108
451
|
def package_count
|
109
452
|
@res_table.package_count
|
110
453
|
end
|
111
454
|
|
455
|
+
# This method only support string resource for now.
|
456
|
+
# find resource by resource id
|
457
|
+
# @param [String] res_id (like '@0x7f010001' or '@string/key')
|
458
|
+
# @param [Hash] opts option
|
459
|
+
# @option opts [String] :lang language code like 'ja', 'cn'...
|
460
|
+
# @option opts [String] :contry cantry code like 'jp'...
|
461
|
+
# @raise [ArgumentError] invalid id format
|
462
|
+
# @note
|
463
|
+
# This method only support string resource for now.
|
464
|
+
# @note
|
465
|
+
# Always return nil if assign not string type res id.
|
466
|
+
# @since 0.5.0
|
467
|
+
def find(rsc_id, opt={})
|
468
|
+
first_pkg.find(rsc_id, opt)
|
469
|
+
end
|
470
|
+
|
471
|
+
# @param [String] hex_id hexoctet format resource id('@0x7f010001')
|
472
|
+
# @return [String] readable resource id ('@string/key')
|
473
|
+
# @since 0.5.0
|
474
|
+
def res_readable_id(hex_id)
|
475
|
+
first_pkg.res_readable_id(hex_id)
|
476
|
+
end
|
477
|
+
|
478
|
+
# convert readable resource id to hex id
|
479
|
+
# @param [String] readable_id readable resource id ('@string/key')
|
480
|
+
# @return [String] hexoctet format resource id('@0x7f010001')
|
481
|
+
# @since 0.5.0
|
482
|
+
def res_hex_id(readable_id)
|
483
|
+
first_pkg.res_hex_id(readable_id)
|
484
|
+
end
|
485
|
+
|
112
486
|
private
|
487
|
+
def first_pkg
|
488
|
+
@packages.first[1]
|
489
|
+
end
|
113
490
|
def parse
|
114
491
|
offset = 0
|
115
492
|
|
116
493
|
while offset < @data.size
|
117
494
|
type = @data[offset, 2].unpack('v')[0]
|
495
|
+
#print "[%#08x] " % offset
|
496
|
+
@packages = {}
|
118
497
|
case type
|
119
498
|
when 0x0001 # RES_STRING_POOL_TYPE
|
120
499
|
@string_pool = ResStringPool.new(@data, offset)
|
121
500
|
offset += @string_pool.size
|
501
|
+
#puts "RES_STRING_POOL_TYPE %#x, %#x" % [@string_pool.size, offset]
|
122
502
|
when 0x0002 # RES_TABLE_TYPE
|
503
|
+
#puts "RES_TABLE_TYPE"
|
123
504
|
@res_table = ResTableHeader.new(@data, offset)
|
124
505
|
offset += @res_table.header_size
|
125
|
-
when 0x0200
|
126
|
-
#
|
127
|
-
|
128
|
-
|
506
|
+
when 0x0200 # RES_TABLE_PACKAGE_TYPE
|
507
|
+
#puts "RES_TABLE_PACKAGE_TYPE"
|
508
|
+
pkg = ResTablePackage.new(@data, offset)
|
509
|
+
pkg.global_string_pool = @string_pool
|
510
|
+
offset += pkg.size
|
511
|
+
@packages[pkg.name] = pkg
|
129
512
|
else
|
130
513
|
raise "chunk type error: type:%#04x" % type
|
131
514
|
end
|
data/ruby_apk.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "ruby_apk"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.5.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["SecureBrain"]
|
12
|
-
s.date = "
|
12
|
+
s.date = "2013-01-04"
|
13
13
|
s.description = "static analysis tool for android apk"
|
14
14
|
s.email = "info@securebrain.co.jp"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -44,6 +44,7 @@ Gem::Specification.new do |s|
|
|
44
44
|
"spec/data/sample_classes.dex",
|
45
45
|
"spec/data/sample_resources.arsc",
|
46
46
|
"spec/data/sample_resources_utf16.arsc",
|
47
|
+
"spec/data/str_resources.arsc",
|
47
48
|
"spec/dex/access_flag_spec.rb",
|
48
49
|
"spec/dex/dex_object_spec.rb",
|
49
50
|
"spec/dex/info_spec.rb",
|
@@ -58,7 +59,7 @@ Gem::Specification.new do |s|
|
|
58
59
|
s.homepage = "https://github.com/SecureBrain/ruby_apk/"
|
59
60
|
s.licenses = ["MIT"]
|
60
61
|
s.require_paths = ["lib"]
|
61
|
-
s.rubygems_version = "1.8.
|
62
|
+
s.rubygems_version = "1.8.23"
|
62
63
|
s.summary = "static analysis tool for android apk"
|
63
64
|
|
64
65
|
if s.respond_to? :specification_version then
|
Binary file
|
data/spec/resource_spec.rb
CHANGED
@@ -83,5 +83,74 @@ describe Android::Resource do
|
|
83
83
|
end
|
84
84
|
end
|
85
85
|
end
|
86
|
+
|
87
|
+
context 'with str_resources.arsc data' do
|
88
|
+
let(:res_data) { File.read(File.expand_path(File.dirname(__FILE__) + '/data/str_resources.arsc')) }
|
89
|
+
subject { resource }
|
90
|
+
describe '#packages' do
|
91
|
+
subject {resource.packages}
|
92
|
+
it { should be_instance_of Hash}
|
93
|
+
it { subject.size.should eq 1 }
|
94
|
+
end
|
95
|
+
describe 'ResTablePackage' do
|
96
|
+
subject { resource.packages.first[1] }
|
97
|
+
it { subject.type(1).should eq 'attr' }
|
98
|
+
it { subject.type(4).should eq 'string' }
|
99
|
+
it { subject.name.should eq 'com.example.sample.ruby_apk' }
|
100
|
+
end
|
101
|
+
describe '#find' do
|
102
|
+
it '@0x7f040000 should return "sample.ruby_apk"' do
|
103
|
+
subject.find('@0x7f040000').should eq 'sample application'
|
104
|
+
end
|
105
|
+
it '@string/app_name should return "sample.ruby_apk"' do
|
106
|
+
subject.find('@string/app_name').should eq 'sample application'
|
107
|
+
end
|
108
|
+
it '@string/hello_world should return "Hello world!"' do
|
109
|
+
subject.find('@string/hello_world').should eq 'Hello world!'
|
110
|
+
end
|
111
|
+
it '@string/app_name should return "sample.ruby_apk"' do
|
112
|
+
subject.find('@string/app_name').should eq 'sample application'
|
113
|
+
end
|
114
|
+
it '@string/app_name with {:lang => "ja"} should return "サンプルアプリ"' do
|
115
|
+
subject.find('@string/app_name', :lang => 'ja').should eq 'サンプルアプリ'
|
116
|
+
end
|
117
|
+
it '@string/hello_world with {:lang => "ja"} should return nil' do
|
118
|
+
subject.find('@string/hello_world', :lang => 'ja').should be_nil
|
119
|
+
end
|
120
|
+
context 'assigns not exist string resource id' do
|
121
|
+
it { expect {subject.find('@string/not_exist') }.to raise_error Android::NotFoundError }
|
122
|
+
it { expect {subject.find('@0x7f040033') }.to raise_error Android::NotFoundError }
|
123
|
+
end
|
124
|
+
context 'assigns not string resource id' do
|
125
|
+
it { subject.find('@layout/activity_main').should be_nil }
|
126
|
+
end
|
127
|
+
context 'assigns invalid format id' do
|
128
|
+
it '"@xxyyxxyy" should raise ArgumentError' do
|
129
|
+
expect{ subject.find('@xxyyxxyy') }.to raise_error(ArgumentError)
|
130
|
+
end
|
131
|
+
it '"@0xff112233445566" should raise ArgumentError' do
|
132
|
+
expect{ subject.find('@0xff112233445566') }.to raise_error(ArgumentError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
describe '#res_readable_id' do
|
137
|
+
it { subject.res_readable_id('@0x7f040000').should eq '@string/app_name' }
|
138
|
+
context 'assigns invalid type' do
|
139
|
+
it { expect{subject.res_readable_id('@0x7f0f0000')}.to raise_error Android::NotFoundError }
|
140
|
+
end
|
141
|
+
context 'assigns invalid key' do
|
142
|
+
it { expect{subject.res_readable_id('@0x7f040033')}.to raise_error Android::NotFoundError }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
describe '#res_hex_id' do
|
146
|
+
it { subject.res_hex_id('@string/app_name').should eq '@0x7f040000' }
|
147
|
+
context 'assigns invalid type' do
|
148
|
+
it { expect{subject.res_readable_id('@not_exist/xxxx')}.to raise_error Android::NotFoundError }
|
149
|
+
end
|
150
|
+
context 'assigns invalid key' do
|
151
|
+
it { expect{subject.res_readable_id('@string/not_exist')}.to raise_error Android::NotFoundError }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
86
155
|
end
|
87
156
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_apk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rubyzip
|
@@ -158,6 +158,7 @@ files:
|
|
158
158
|
- spec/data/sample_classes.dex
|
159
159
|
- spec/data/sample_resources.arsc
|
160
160
|
- spec/data/sample_resources_utf16.arsc
|
161
|
+
- spec/data/str_resources.arsc
|
161
162
|
- spec/dex/access_flag_spec.rb
|
162
163
|
- spec/dex/dex_object_spec.rb
|
163
164
|
- spec/dex/info_spec.rb
|
@@ -183,7 +184,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
183
184
|
version: '0'
|
184
185
|
segments:
|
185
186
|
- 0
|
186
|
-
hash:
|
187
|
+
hash: 671022741244016522
|
187
188
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
188
189
|
none: false
|
189
190
|
requirements:
|
@@ -192,7 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
192
193
|
version: '0'
|
193
194
|
requirements: []
|
194
195
|
rubyforge_project:
|
195
|
-
rubygems_version: 1.8.
|
196
|
+
rubygems_version: 1.8.23
|
196
197
|
signing_key:
|
197
198
|
specification_version: 3
|
198
199
|
summary: static analysis tool for android apk
|