ruby_android_apk 0.7.7.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.
- checksums.yaml +7 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +22 -0
- data/README.md +157 -0
- data/Rakefile +43 -0
- data/VERSION +1 -0
- data/lib/android/apk.rb +207 -0
- data/lib/android/axml_parser.rb +174 -0
- data/lib/android/dex.rb +92 -0
- data/lib/android/dex/access_flag.rb +74 -0
- data/lib/android/dex/dex_object.rb +475 -0
- data/lib/android/dex/info.rb +151 -0
- data/lib/android/dex/utils.rb +45 -0
- data/lib/android/layout.rb +43 -0
- data/lib/android/manifest.rb +249 -0
- data/lib/android/resource.rb +531 -0
- data/lib/android/utils.rb +55 -0
- data/lib/ruby_apk.rb +7 -0
- data/spec/apk_spec.rb +332 -0
- data/spec/axml_parser_spec.rb +67 -0
- data/spec/data/sample.apk +0 -0
- data/spec/data/sample_AndroidManifest.xml +0 -0
- data/spec/data/sample_classes.dex +0 -0
- data/spec/data/sample_resources.arsc +0 -0
- data/spec/data/sample_resources_utf16.arsc +0 -0
- data/spec/data/str_resources.arsc +0 -0
- data/spec/dex/access_flag_spec.rb +42 -0
- data/spec/dex/dex_object_spec.rb +118 -0
- data/spec/dex/info_spec.rb +121 -0
- data/spec/dex/utils_spec.rb +56 -0
- data/spec/dex_spec.rb +59 -0
- data/spec/layout_spec.rb +41 -0
- data/spec/manifest_spec.rb +228 -0
- data/spec/resource_spec.rb +170 -0
- data/spec/ruby_apk_spec.rb +4 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/utils_spec.rb +95 -0
- metadata +185 -0
@@ -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
data/spec/apk_spec.rb
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'tempfile'
|
3
|
+
require 'zip'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'digest/sha2'
|
6
|
+
require 'digest/md5'
|
7
|
+
|
8
|
+
class TempApk
|
9
|
+
attr_reader :path
|
10
|
+
def initialize
|
11
|
+
@tmp = Tempfile.open('apk_spec')
|
12
|
+
@path = @tmp.path
|
13
|
+
@tmp.close! # delete file
|
14
|
+
append("AndroidManifest.xml", "hogehoge")
|
15
|
+
append("resources.arsc", "hogehoge")
|
16
|
+
end
|
17
|
+
def destroy!
|
18
|
+
File.unlink(@path) if File.exist? @path
|
19
|
+
end
|
20
|
+
def append(entry_name, data)
|
21
|
+
Zip::File.open(@path, Zip::File::CREATE) { |zip|
|
22
|
+
zip.get_output_stream(entry_name) {|f| f.write data }
|
23
|
+
}
|
24
|
+
end
|
25
|
+
def remove(entry_name)
|
26
|
+
Zip::File.open(@path, Zip::File::CREATE) { |zip|
|
27
|
+
zip.remove(entry_name)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe Android::Apk do
|
33
|
+
before do
|
34
|
+
$stderr.reopen('/dev/null','w')
|
35
|
+
end
|
36
|
+
let(:tmp_apk) { TempApk.new }
|
37
|
+
let(:tmp_path) { tmp_apk.path }
|
38
|
+
let(:apk) { Android::Apk.new(tmp_path) }
|
39
|
+
subject { apk }
|
40
|
+
|
41
|
+
after do
|
42
|
+
tmp_apk.destroy!
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#initialize" do
|
46
|
+
let(:path) { tmp_path }
|
47
|
+
subject { Android::Apk.new(path) }
|
48
|
+
context "with not exist path" do
|
49
|
+
let(:path) { "not exist path" }
|
50
|
+
it { expect{ subject }.to raise_error Android::NotFoundError }
|
51
|
+
end
|
52
|
+
context "with not zip file path" do
|
53
|
+
let(:path) { __FILE__ } # not zip file
|
54
|
+
it { expect{ subject }.to raise_error Android::NotApkFileError }
|
55
|
+
end
|
56
|
+
context "with zip(and non apk) file" do
|
57
|
+
before do
|
58
|
+
tmp_apk.append('hoge.txt', 'hogehoge')
|
59
|
+
tmp_apk.remove('AndroidManifest.xml')
|
60
|
+
end
|
61
|
+
it { expect{ subject }.to raise_error Android::NotApkFileError }
|
62
|
+
end
|
63
|
+
context "with zip includes AndroidManifest.xml" do
|
64
|
+
it { should be_a_instance_of Android::Apk }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#path" do
|
69
|
+
subject { apk.path }
|
70
|
+
it "should equals initialized path" do
|
71
|
+
should == tmp_path
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#manifest" do
|
76
|
+
subject { apk.manifest }
|
77
|
+
|
78
|
+
context "when Manifest parse is succeeded." do
|
79
|
+
let(:mock_mani) { mock(Android::Manifest) }
|
80
|
+
|
81
|
+
before do
|
82
|
+
end
|
83
|
+
it "should return manifest object" do
|
84
|
+
Android::Manifest.should_receive(:new).and_return(mock_mani)
|
85
|
+
subject.should == mock_mani
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "when Manifest parse is failed" do
|
90
|
+
it 'should return nil' do
|
91
|
+
Android::Manifest.should_receive(:new).and_raise(Android::AXMLParser::ReadError)
|
92
|
+
subject.should be_nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#dex" do
|
98
|
+
let(:mock_dex) { mock(Android::Dex) }
|
99
|
+
subject { apk.dex }
|
100
|
+
context "when there is no dex file" do
|
101
|
+
it { should be_nil }
|
102
|
+
end
|
103
|
+
context "when invalid dex file" do
|
104
|
+
before do
|
105
|
+
tmp_apk.append("classes.dex", "invalid dex")
|
106
|
+
end
|
107
|
+
it { should be_nil }
|
108
|
+
end
|
109
|
+
context "with mock classes.dex file" do
|
110
|
+
before do
|
111
|
+
tmp_apk.append("classes.dex", "mock data")
|
112
|
+
end
|
113
|
+
it "should return mock value" do
|
114
|
+
Android::Dex.should_receive(:new).with("mock data").and_return(mock_dex)
|
115
|
+
subject.should == mock_dex
|
116
|
+
end
|
117
|
+
end
|
118
|
+
context "with real classes.dex file" do
|
119
|
+
before do
|
120
|
+
dex_path = File.expand_path(File.dirname(__FILE__) + '/data/sample_classes.dex')
|
121
|
+
tmp_apk.append("classes.dex", File.open(dex_path, "rb") {|f| f.read })
|
122
|
+
end
|
123
|
+
it { should be_instance_of Android::Dex }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
its(:bindata) { should be_instance_of String }
|
128
|
+
describe '#bindata' do
|
129
|
+
specify 'encoding should be ASCII-8BIT' do
|
130
|
+
subject.bindata.encoding.should eq Encoding::ASCII_8BIT
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '#resource' do
|
135
|
+
let(:mock_rsc) { mock(Android::Resource) }
|
136
|
+
subject { apk.resource }
|
137
|
+
it "should return manifest object" do
|
138
|
+
Android::Resource.should_receive(:new).and_return(mock_rsc)
|
139
|
+
subject.should == mock_rsc
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe "#size" do
|
144
|
+
subject { apk.size }
|
145
|
+
it "should return apk file size" do
|
146
|
+
should == File.size(tmp_path)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "#digest" do
|
151
|
+
let(:data) { File.open(tmp_apk.path, 'rb'){|f| f.read } }
|
152
|
+
subject { apk.digest(type) }
|
153
|
+
context "when type is sha1" do
|
154
|
+
let(:type) { :sha1 }
|
155
|
+
it "should return sha1 digest" do
|
156
|
+
should eq Digest::SHA1.hexdigest(data)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
context "when type is sha256" do
|
160
|
+
let(:type) { :sha256 }
|
161
|
+
it "should return sha256 digest" do
|
162
|
+
should == Digest::SHA256.hexdigest(data)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
context "when type is md5" do
|
166
|
+
let(:type) { :md5 }
|
167
|
+
it "should return md5 digest" do
|
168
|
+
should == Digest::MD5.hexdigest(data)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
context "when type is unkown symbol" do
|
172
|
+
let(:type) { :unknown }
|
173
|
+
it do
|
174
|
+
expect { subject }.to raise_error(ArgumentError)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
context "when type is not symbol(String: 'sha1')" do
|
178
|
+
let(:type) { 'sha1' }
|
179
|
+
it { expect { subject }.to raise_error(ArgumentError) }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe '#time' do
|
184
|
+
subject { apk.time }
|
185
|
+
it { should be_kind_of Time }
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#each_file" do
|
189
|
+
before do
|
190
|
+
tmp_apk.append("hoge.txt", "aaaaaaa")
|
191
|
+
end
|
192
|
+
it { expect { |b| apk.each_file(&b) }.to yield_successive_args(Array, Array, Array) }
|
193
|
+
let(:each_file_result ) {
|
194
|
+
result = []
|
195
|
+
apk.each_file do |name, data|
|
196
|
+
result << [name, data]
|
197
|
+
end
|
198
|
+
result
|
199
|
+
}
|
200
|
+
|
201
|
+
it "should invoke block with all file" do
|
202
|
+
each_file_result.should have(3).items
|
203
|
+
each_file_result.should include(['AndroidManifest.xml', 'hogehoge'])
|
204
|
+
each_file_result.should include(['hoge.txt','aaaaaaa'])
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe '#file' do
|
209
|
+
let(:data) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaa' }
|
210
|
+
let(:path) { 'hoge.txt' }
|
211
|
+
subject { apk.file(path) }
|
212
|
+
|
213
|
+
before do
|
214
|
+
tmp_apk.append('hoge.txt', data)
|
215
|
+
end
|
216
|
+
context 'assigns exist path' do
|
217
|
+
it 'should equal file data' do
|
218
|
+
should eq data
|
219
|
+
end
|
220
|
+
end
|
221
|
+
context 'assigns not exist path' do
|
222
|
+
let(:path) { 'not_exist_path.txt' }
|
223
|
+
it { expect { subject }.to raise_error(Android::NotFoundError) }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe '#each_entry' do
|
228
|
+
before do
|
229
|
+
tmp_apk.append("hoge.txt", "aaaaaaa")
|
230
|
+
end
|
231
|
+
it { expect { |b| apk.each_entry(&b) }.to yield_successive_args(Zip::Entry, Zip::Entry, Zip::Entry) }
|
232
|
+
end
|
233
|
+
|
234
|
+
describe '#entry' do
|
235
|
+
subject { apk.entry(entry_name) }
|
236
|
+
context 'assigns exist entry' do
|
237
|
+
let(:entry_name) { 'AndroidManifest.xml' }
|
238
|
+
it { should be_instance_of Zip::Entry }
|
239
|
+
end
|
240
|
+
context 'assigns not exist entry name' do
|
241
|
+
let(:entry_name) { 'not_exist_path' }
|
242
|
+
it { expect{ subject }.to raise_error(Android::NotFoundError) }
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
describe "#find" do
|
247
|
+
before do
|
248
|
+
tmp_apk.append("hoge.txt", "aaaaaaa")
|
249
|
+
end
|
250
|
+
it "should return matched array" do
|
251
|
+
array = apk.find do |name, data|
|
252
|
+
name == "hoge.txt"
|
253
|
+
end
|
254
|
+
array.should be_instance_of Array
|
255
|
+
array.should have(1).item
|
256
|
+
array[0] == "hoge.txt" # returns filename
|
257
|
+
end
|
258
|
+
context "when no entry is matched" do
|
259
|
+
it "should return emtpy array" do
|
260
|
+
array = apk.find do |name, dota|
|
261
|
+
false # nothing matched
|
262
|
+
end
|
263
|
+
array.should be_instance_of Array
|
264
|
+
array.should be_empty
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe "#icon" do
|
270
|
+
context "with real apk file" do
|
271
|
+
let(:tmp_path){ File.expand_path(File.dirname(__FILE__) + '/data/sample.apk') }
|
272
|
+
subject { apk.icon }
|
273
|
+
it { should be_a Hash }
|
274
|
+
it { should have(3).items }
|
275
|
+
it { subject.keys.should =~ ["res/drawable-hdpi/ic_launcher.png", "res/drawable-ldpi/ic_launcher.png", "res/drawable-mdpi/ic_launcher.png"]}
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
describe "#icon" do
|
280
|
+
context "with real new apk file" do
|
281
|
+
let(:tmp_path){ File.expand_path(File.dirname(__FILE__) + '/data/sample_new.apk') }
|
282
|
+
subject { apk.icon }
|
283
|
+
it { should be_a Hash }
|
284
|
+
it { should have(4).items }
|
285
|
+
it { subject.keys.should =~ ["res/mipmap-xxhdpi-v4/ic_launcher.png", "res/mipmap-hdpi-v4/ic_launcher.png", "res/mipmap-mdpi-v4/ic_launcher.png", "res/mipmap-xhdpi-v4/ic_launcher.png"]}
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
describe '#signs' do
|
290
|
+
context 'with sample apk file' do
|
291
|
+
let(:tmp_path){ File.expand_path(File.dirname(__FILE__) + '/data/sample.apk') }
|
292
|
+
subject { apk.signs }
|
293
|
+
it { should be_a Hash }
|
294
|
+
it { should have(1).item }
|
295
|
+
it { should have_key('META-INF/CERT.RSA') }
|
296
|
+
it { subject['META-INF/CERT.RSA'].should be_a OpenSSL::PKCS7 }
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
describe '#signs' do
|
301
|
+
context 'with new sample apk file' do
|
302
|
+
let(:tmp_path){ File.expand_path(File.dirname(__FILE__) + '/data/sample_new.apk') }
|
303
|
+
subject { apk.signs }
|
304
|
+
it { should be_a Hash }
|
305
|
+
it { should have(1).item }
|
306
|
+
it { should have_key('META-INF/CERT.RSA') }
|
307
|
+
it { subject['META-INF/CERT.RSA'].should be_a OpenSSL::PKCS7 }
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe '#certficates' do
|
312
|
+
context 'with sample apk file' do
|
313
|
+
let(:tmp_path){ File.expand_path(File.dirname(__FILE__) + '/data/sample.apk') }
|
314
|
+
subject { apk.certificates }
|
315
|
+
it { should be_a Hash }
|
316
|
+
it { should have(1).item }
|
317
|
+
it { should have_key('META-INF/CERT.RSA') }
|
318
|
+
it { subject['META-INF/CERT.RSA'].should be_a OpenSSL::X509::Certificate }
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe '#certficates' do
|
323
|
+
context 'with new sample apk file' do
|
324
|
+
let(:tmp_path){ File.expand_path(File.dirname(__FILE__) + '/data/sample_new.apk') }
|
325
|
+
subject { apk.certificates }
|
326
|
+
it { should be_a Hash }
|
327
|
+
it { should have(1).item }
|
328
|
+
it { should have_key('META-INF/CERT.RSA') }
|
329
|
+
it { subject['META-INF/CERT.RSA'].should be_a OpenSSL::X509::Certificate }
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
|
+
|
4
|
+
describe Android::AXMLParser do
|
5
|
+
let(:bin_xml_path){ File.expand_path(File.dirname(__FILE__) + '/data/sample_AndroidManifest.xml') }
|
6
|
+
let(:bin_xml){ File.open(bin_xml_path, 'rb') {|f| f.read } }
|
7
|
+
let(:axmlparser){ Android::AXMLParser.new(bin_xml) }
|
8
|
+
|
9
|
+
describe "#parse" do
|
10
|
+
|
11
|
+
subject { axmlparser.parse }
|
12
|
+
context 'with sample_AndroidManifest.xml' do
|
13
|
+
it { should be_instance_of(REXML::Document) }
|
14
|
+
specify 'root element should be <manifest> element' do
|
15
|
+
subject.root.name.should eq 'manifest'
|
16
|
+
end
|
17
|
+
specify 'should have 2 <uses-permission> elements' do
|
18
|
+
subject.get_elements('/manifest/uses-permission').should have(2).items
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'with nil data as binary xml' do
|
23
|
+
let(:bin_xml) { nil }
|
24
|
+
specify { expect{ subject }.to raise_error }
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#strings" do
|
30
|
+
context 'with sample_AndroidManifest.xml' do
|
31
|
+
subject { axmlparser.strings }
|
32
|
+
before do
|
33
|
+
axmlparser.parse
|
34
|
+
end
|
35
|
+
it { should be_instance_of(Array) }
|
36
|
+
|
37
|
+
# ugh!! the below test cases depend on sample_AndroidManifest.xml
|
38
|
+
it { should have(26).items} # in sample manifest.
|
39
|
+
it { should include("versionCode") }
|
40
|
+
it { should include("versionName") }
|
41
|
+
it { should include("minSdkVersion") }
|
42
|
+
it { should include("package") }
|
43
|
+
it { should include("manifest") }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#convert_value' do
|
48
|
+
let(:axmlparser){ Android::AXMLParser.new('') }
|
49
|
+
subject { axmlparser.convert_value(str_id, flags, val) }
|
50
|
+
context 'when parsing boolean attribute' do
|
51
|
+
let(:str_id) { 0xFFFFFFFF }
|
52
|
+
let(:flags) { 0x12000008 }
|
53
|
+
context 'and value is 0x01' do
|
54
|
+
let(:val) { 0x01 }
|
55
|
+
it { should be_true }
|
56
|
+
end
|
57
|
+
context 'and value is 0xFFFFFFF' do
|
58
|
+
let(:val) { 0xFFFFFFFF }
|
59
|
+
it { should be_true }
|
60
|
+
end
|
61
|
+
context 'and value is 0x00' do
|
62
|
+
let(:val) { 0x00 }
|
63
|
+
it { should be_false }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|