ipa_reader 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -0
- data/History.txt +4 -0
- data/README.txt +63 -0
- data/Rakefile +18 -0
- data/bin/ipa_reader +7 -0
- data/lib/ipa_reader/ipa_file.rb +48 -0
- data/lib/ipa_reader/plist_binary.rb +490 -0
- data/lib/ipa_reader.rb +65 -0
- data/spec/ipa_reader_spec.rb +6 -0
- data/spec/spec_helper.rb +15 -0
- data/test/MultiG.ipa +0 -0
- data/test/test_ipa_reader.rb +42 -0
- data/version.txt +1 -0
- metadata +97 -0
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.8.7@ipa_reader
|
data/History.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
ipa_reader
|
2
|
+
by Nicholas Schlueter
|
3
|
+
http://twitter.com/schlu
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Reads metadata form iPhone Package Archive Files (ipa).
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
I am using this gem to get version to build the over the air iPhone Ad Hoc distribution plist file.
|
12
|
+
|
13
|
+
== USAGE:
|
14
|
+
|
15
|
+
irb > require 'rubygems'
|
16
|
+
=> true
|
17
|
+
irb > require 'ipa_reader'
|
18
|
+
=> true
|
19
|
+
irb > ipa_file = IpaReader::IpaFile.new("/path/to/file.ipa")
|
20
|
+
=> #<IpaReader::IpaFile:0x1012a9458>
|
21
|
+
irb > ipa_file.version
|
22
|
+
=> "1.2.2.4"
|
23
|
+
irb > ipa_file.name
|
24
|
+
=> "MultiG"
|
25
|
+
irb > ipa_file.target_os_version
|
26
|
+
=> "4.1"
|
27
|
+
irb > ipa_file.minimum_os_version
|
28
|
+
=> "3.1"
|
29
|
+
irb > ipa_file.url_schemes
|
30
|
+
=> []
|
31
|
+
irb > ipa_file.bundle_identifier
|
32
|
+
=> "com.dcrails.multig"
|
33
|
+
irb > ipa_file.icon_prerendered
|
34
|
+
=> false
|
35
|
+
|
36
|
+
== INSTALL:
|
37
|
+
|
38
|
+
gem install ipa_reader
|
39
|
+
|
40
|
+
== LICENSE:
|
41
|
+
|
42
|
+
(The MIT License)
|
43
|
+
|
44
|
+
Copyright (c) 2010
|
45
|
+
|
46
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
47
|
+
a copy of this software and associated documentation files (the
|
48
|
+
'Software'), to deal in the Software without restriction, including
|
49
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
50
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
51
|
+
permit persons to whom the Software is furnished to do so, subject to
|
52
|
+
the following conditions:
|
53
|
+
|
54
|
+
The above copyright notice and this permission notice shall be
|
55
|
+
included in all copies or substantial portions of the Software.
|
56
|
+
|
57
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
58
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
59
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
60
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
61
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
62
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
63
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
begin
|
3
|
+
require 'bones'
|
4
|
+
rescue LoadError
|
5
|
+
abort '### Please install the "bones" gem ###'
|
6
|
+
end
|
7
|
+
|
8
|
+
task :default => 'test:run'
|
9
|
+
task 'gem:release' => 'test:run'
|
10
|
+
|
11
|
+
Bones {
|
12
|
+
name 'ipa_reader'
|
13
|
+
authors 'Nicholas Schlueter'
|
14
|
+
email 'schlueter@gmail.com'
|
15
|
+
url 'http://github.com/schlueter/Ipa-Reader'
|
16
|
+
depends_on "zip", "2.0.2"
|
17
|
+
}
|
18
|
+
|
data/bin/ipa_reader
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
begin
|
2
|
+
require 'zip'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems'
|
5
|
+
require 'zip'
|
6
|
+
end
|
7
|
+
module IpaReader
|
8
|
+
class IpaFile
|
9
|
+
attr_accessor :plist
|
10
|
+
def initialize(file_path)
|
11
|
+
info_plist_file = nil
|
12
|
+
Zip::ZipFile.foreach(file_path) { |f| info_plist_file = f if f.name.match(/\/Info.plist/) }
|
13
|
+
self.plist = Plist::Binary.decode_binary_plist(info_plist_file.get_input_stream.read)
|
14
|
+
end
|
15
|
+
|
16
|
+
def version
|
17
|
+
plist["CFBundleVersion"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
plist["CFBundleDisplayName"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def target_os_version
|
25
|
+
plist["DTPlatformVersion"].match(/[\d\.]*/)[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
def minimum_os_version
|
29
|
+
plist["MinimumOSVersion"].match(/[\d\.]*/)[0]
|
30
|
+
end
|
31
|
+
|
32
|
+
def url_schemes
|
33
|
+
if plist["CFBundleURLTypes"] && plist["CFBundleURLTypes"][0] && plist["CFBundleURLTypes"][0]["CFBundleURLSchemes"]
|
34
|
+
plist["CFBundleURLTypes"][0]["CFBundleURLSchemes"]
|
35
|
+
else
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def bundle_identifier
|
41
|
+
plist["CFBundleIdentifier"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def icon_prerendered
|
45
|
+
plist["UIPrerenderedIcon"] == true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,490 @@
|
|
1
|
+
require "date"
|
2
|
+
require "nkf"
|
3
|
+
require "set"
|
4
|
+
require "stringio"
|
5
|
+
|
6
|
+
module IpaReader
|
7
|
+
module Plist
|
8
|
+
module Binary
|
9
|
+
# Encodes +obj+ as a binary property list. If +obj+ is an Array, Hash, or
|
10
|
+
# Set, the property list includes its contents.
|
11
|
+
def self.binary_plist(obj)
|
12
|
+
encoded_objs = flatten_collection(obj)
|
13
|
+
ref_byte_size = min_byte_size(encoded_objs.length - 1)
|
14
|
+
encoded_objs.collect! {|o| binary_plist_obj(o, ref_byte_size)}
|
15
|
+
# Write header and encoded objects.
|
16
|
+
plist = "bplist00" + encoded_objs.join
|
17
|
+
# Write offset table.
|
18
|
+
offset_table_addr = plist.length
|
19
|
+
offset = 8
|
20
|
+
offset_table = []
|
21
|
+
encoded_objs.each do |o|
|
22
|
+
offset_table << offset
|
23
|
+
offset += o.length
|
24
|
+
end
|
25
|
+
offset_byte_size = min_byte_size(offset)
|
26
|
+
offset_table.each do |offset|
|
27
|
+
plist += pack_int(offset, offset_byte_size)
|
28
|
+
end
|
29
|
+
# Write trailer.
|
30
|
+
plist += "\0\0\0\0\0\0" # Six unused bytes
|
31
|
+
plist += [
|
32
|
+
offset_byte_size,
|
33
|
+
ref_byte_size,
|
34
|
+
encoded_objs.length >> 32, encoded_objs.length & 0xffffffff,
|
35
|
+
0, 0, # Index of root object
|
36
|
+
offset_table_addr >> 32, offset_table_addr & 0xffffffff
|
37
|
+
].pack("CCNNNNNN")
|
38
|
+
plist
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.decode_binary_plist(plist)
|
42
|
+
# Check header.
|
43
|
+
unless plist[0, 6] == "bplist"
|
44
|
+
raise ArgumentError, "argument is not a binary property list"
|
45
|
+
end
|
46
|
+
version = plist[6, 2]
|
47
|
+
unless version == "00"
|
48
|
+
raise ArgumentError,
|
49
|
+
"don't know how to decode format version #{version}"
|
50
|
+
end
|
51
|
+
# Read trailer.
|
52
|
+
trailer = plist[-26, 26].unpack("CCNNNNNN")
|
53
|
+
offset_byte_size = trailer[0]
|
54
|
+
ref_byte_size = trailer[1]
|
55
|
+
encoded_objs_length = combine_ints(32, trailer[2], trailer[3])
|
56
|
+
root_index = combine_ints(32, trailer[4], trailer[5])
|
57
|
+
offset_table_addr = combine_ints(32, trailer[6], trailer[7])
|
58
|
+
# Decode objects.
|
59
|
+
root_offset = offset_for_index(plist, offset_table_addr,
|
60
|
+
offset_byte_size, root_index)
|
61
|
+
root_obj = decode_binary_plist_obj(plist, root_offset, ref_byte_size)
|
62
|
+
unflatten_collection(root_obj, [root_obj], plist, offset_table_addr,
|
63
|
+
offset_byte_size, ref_byte_size)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# These marker bytes are prefixed to objects in a binary property list to
|
69
|
+
# indicate the type of the object.
|
70
|
+
CFBinaryPlistMarkerNull = 0x00 # :nodoc:
|
71
|
+
CFBinaryPlistMarkerFalse = 0x08 # :nodoc:
|
72
|
+
CFBinaryPlistMarkerTrue = 0x09 # :nodoc:
|
73
|
+
CFBinaryPlistMarkerFill = 0x0F # :nodoc:
|
74
|
+
CFBinaryPlistMarkerInt = 0x10 # :nodoc:
|
75
|
+
CFBinaryPlistMarkerReal = 0x20 # :nodoc:
|
76
|
+
CFBinaryPlistMarkerDate = 0x33 # :nodoc:
|
77
|
+
CFBinaryPlistMarkerData = 0x40 # :nodoc:
|
78
|
+
CFBinaryPlistMarkerASCIIString = 0x50 # :nodoc:
|
79
|
+
CFBinaryPlistMarkerUnicode16String = 0x60 # :nodoc:
|
80
|
+
CFBinaryPlistMarkerUID = 0x80 # :nodoc:
|
81
|
+
CFBinaryPlistMarkerArray = 0xA0 # :nodoc:
|
82
|
+
CFBinaryPlistMarkerSet = 0xC0 # :nodoc:
|
83
|
+
CFBinaryPlistMarkerDict = 0xD0 # :nodoc:
|
84
|
+
|
85
|
+
# POSIX uses a reference time of 1970-01-01T00:00:00Z; Cocoa's reference
|
86
|
+
# time is in 2001. This interval is for converting between the two.
|
87
|
+
NSTimeIntervalSince1970 = 978307200.0 # :nodoc:
|
88
|
+
|
89
|
+
# Takes an object (nominally a collection, like an Array, Set, or Hash, but
|
90
|
+
# any object is acceptable) and flattens it into a one-dimensional array.
|
91
|
+
# Non-collection objects appear in the array as-is, but the contents of
|
92
|
+
# Arrays, Sets, and Hashes are modified like so: (1) The contents of the
|
93
|
+
# collection are added, one-by-one, to the one-dimensional array. (2) The
|
94
|
+
# collection itself is modified so that it contains indexes pointing to the
|
95
|
+
# objects in the one-dimensional array. Here's an example with an Array:
|
96
|
+
#
|
97
|
+
# ary = [:a, :b, :c]
|
98
|
+
# flatten_collection(ary) # => [[1, 2, 3], :a, :b, :c]
|
99
|
+
#
|
100
|
+
# In the case of a Hash, keys and values are both appended to the one-
|
101
|
+
# dimensional array and then replaced with indexes.
|
102
|
+
#
|
103
|
+
# hsh = {:a => "blue", :b => "purple", :c => "green"}
|
104
|
+
# flatten_collection(hsh)
|
105
|
+
# # => [{1 => 2, 3 => 4, 5 => 6}, :a, "blue", :b, "purple", :c, "green"]
|
106
|
+
#
|
107
|
+
# An object will never be added to the one-dimensional array twice. If a
|
108
|
+
# collection refers to an object more than once, the object will be added
|
109
|
+
# to the one-dimensional array only once.
|
110
|
+
#
|
111
|
+
# ary = [:a, :a, :a]
|
112
|
+
# flatten_collection(ary) # => [[1, 1, 1], :a]
|
113
|
+
#
|
114
|
+
# The +obj_list+ and +id_refs+ parameters are private; they're used for
|
115
|
+
# descending into sub-collections recursively.
|
116
|
+
def self.flatten_collection(collection, obj_list = [], id_refs = {})
|
117
|
+
case collection
|
118
|
+
when Array, Set
|
119
|
+
if id_refs[collection.object_id]
|
120
|
+
return obj_list[id_refs[collection.object_id]]
|
121
|
+
end
|
122
|
+
obj_refs = collection.class.new
|
123
|
+
id_refs[collection.object_id] = obj_list.length
|
124
|
+
obj_list << obj_refs
|
125
|
+
collection.each do |obj|
|
126
|
+
flatten_collection(obj, obj_list, id_refs)
|
127
|
+
obj_refs << id_refs[obj.object_id]
|
128
|
+
end
|
129
|
+
return obj_list
|
130
|
+
when Hash
|
131
|
+
if id_refs[collection.object_id]
|
132
|
+
return obj_list[id_refs[collection.object_id]]
|
133
|
+
end
|
134
|
+
obj_refs = {}
|
135
|
+
id_refs[collection.object_id] = obj_list.length
|
136
|
+
obj_list << obj_refs
|
137
|
+
collection.each do |key, value|
|
138
|
+
key = key.to_s if key.is_a?(Symbol)
|
139
|
+
flatten_collection(key, obj_list, id_refs)
|
140
|
+
flatten_collection(value, obj_list, id_refs)
|
141
|
+
obj_refs[id_refs[key.object_id]] = id_refs[value.object_id]
|
142
|
+
end
|
143
|
+
return obj_list
|
144
|
+
else
|
145
|
+
unless id_refs[collection.object_id]
|
146
|
+
id_refs[collection.object_id] = obj_list.length
|
147
|
+
obj_list << collection
|
148
|
+
end
|
149
|
+
return obj_list
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.unflatten_collection(collection, obj_list, plist,
|
154
|
+
offset_table_addr, offset_byte_size, ref_byte_size)
|
155
|
+
case collection
|
156
|
+
when Array, Set
|
157
|
+
collection.collect! do |index|
|
158
|
+
if obj = obj_list[index]
|
159
|
+
obj
|
160
|
+
else
|
161
|
+
offset = offset_for_index(plist, offset_table_addr, offset_byte_size,
|
162
|
+
index)
|
163
|
+
obj = decode_binary_plist_obj(plist, offset, ref_byte_size)
|
164
|
+
obj_list[index] = obj
|
165
|
+
unflatten_collection(obj, obj_list, plist, offset_table_addr,
|
166
|
+
offset_byte_size, ref_byte_size)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
when Hash
|
170
|
+
hsh = {}
|
171
|
+
collection.each do |key, value|
|
172
|
+
unless key_obj = obj_list[key]
|
173
|
+
offset = offset_for_index(plist, offset_table_addr, offset_byte_size,
|
174
|
+
key)
|
175
|
+
key_obj = decode_binary_plist_obj(plist, offset, ref_byte_size)
|
176
|
+
obj_list[key] = key_obj
|
177
|
+
key_obj = unflatten_collection(key_obj, obj_list, plist,
|
178
|
+
offset_table_addr, offset_byte_size, ref_byte_size)
|
179
|
+
end
|
180
|
+
unless value_obj = obj_list[value]
|
181
|
+
offset = offset_for_index(plist, offset_table_addr, offset_byte_size,
|
182
|
+
value)
|
183
|
+
value_obj = decode_binary_plist_obj(plist, offset, ref_byte_size)
|
184
|
+
obj_list[value] = value_obj
|
185
|
+
value_obj = unflatten_collection(value_obj, obj_list, plist,
|
186
|
+
offset_table_addr, offset_byte_size, ref_byte_size)
|
187
|
+
end
|
188
|
+
hsh[key_obj] = value_obj
|
189
|
+
end
|
190
|
+
collection.replace(hsh)
|
191
|
+
end
|
192
|
+
return collection
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns a binary property list fragment that represents +obj+. The
|
196
|
+
# returned string is not a complete property list, just a fragment that
|
197
|
+
# describes +obj+, and is not useful without a header, offset table, and
|
198
|
+
# trailer.
|
199
|
+
#
|
200
|
+
# The following classes are recognized: String, Float, Integer, the Boolean
|
201
|
+
# classes, Time, IO, StringIO, Array, Set, and Hash. IO and StringIO
|
202
|
+
# objects are rewound, read, and the contents stored as data (i.e., Cocoa
|
203
|
+
# applications will decode them as NSData). All other classes are dumped
|
204
|
+
# with Marshal and stored as data.
|
205
|
+
#
|
206
|
+
# Note that subclasses of the supported classes will be encoded as though
|
207
|
+
# they were the supported superclass. Thus, a subclass of (for example)
|
208
|
+
# String will be encoded and decoded as a String, not as the subclass:
|
209
|
+
#
|
210
|
+
# class ExampleString < String
|
211
|
+
# ...
|
212
|
+
# end
|
213
|
+
#
|
214
|
+
# s = ExampleString.new("disquieting plantlike mystery")
|
215
|
+
# encoded_s = binary_plist_obj(s)
|
216
|
+
# decoded_s = decode_binary_plist_obj(encoded_s)
|
217
|
+
# puts decoded_s.class # => String
|
218
|
+
#
|
219
|
+
# +ref_byte_size+ is the number of bytes to use for storing references to
|
220
|
+
# other objects.
|
221
|
+
def self.binary_plist_obj(obj, ref_byte_size = 4)
|
222
|
+
case obj
|
223
|
+
when String
|
224
|
+
obj = obj.to_s if obj.is_a?(Symbol)
|
225
|
+
# This doesn't really work. NKF's guess method is really, really bad
|
226
|
+
# at discovering UTF8 when only a handful of characters are multi-byte.
|
227
|
+
encoding = NKF.guess2(obj)
|
228
|
+
if encoding == NKF::ASCII && obj =~ /[\x80-\xff]/
|
229
|
+
encoding = NKF::UTF8
|
230
|
+
end
|
231
|
+
if [NKF::ASCII, NKF::BINARY, NKF::UNKNOWN].include?(encoding)
|
232
|
+
result = (CFBinaryPlistMarkerASCIIString |
|
233
|
+
(obj.length < 15 ? obj.length : 0xf)).chr
|
234
|
+
result += binary_plist_obj(obj.length) if obj.length >= 15
|
235
|
+
result += obj
|
236
|
+
return result
|
237
|
+
else
|
238
|
+
# Convert to UTF8.
|
239
|
+
if encoding == NKF::UTF8
|
240
|
+
utf8 = obj
|
241
|
+
else
|
242
|
+
utf8 = NKF.nkf("-m0 -w", obj)
|
243
|
+
end
|
244
|
+
# Decode each character's UCS codepoint.
|
245
|
+
codepoints = []
|
246
|
+
i = 0
|
247
|
+
while i < utf8.length
|
248
|
+
byte = utf8[i]
|
249
|
+
if byte & 0xe0 == 0xc0
|
250
|
+
codepoints << ((byte & 0x1f) << 6) + (utf8[i+1] & 0x3f)
|
251
|
+
i += 1
|
252
|
+
elsif byte & 0xf0 == 0xe0
|
253
|
+
codepoints << ((byte & 0xf) << 12) + ((utf8[i+1] & 0x3f) << 6) +
|
254
|
+
(utf8[i+2] & 0x3f)
|
255
|
+
i += 2
|
256
|
+
elsif byte & 0xf8 == 0xf0
|
257
|
+
codepoints << ((byte & 0xe) << 18) + ((utf8[i+1] & 0x3f) << 12) +
|
258
|
+
((utf8[i+2] & 0x3f) << 6) + (utf8[i+3] & 0x3f)
|
259
|
+
i += 3
|
260
|
+
else
|
261
|
+
codepoints << byte
|
262
|
+
end
|
263
|
+
if codepoints.last > 0xffff
|
264
|
+
raise(ArgumentError, "codepoint too high - only the Basic Multilingual Plane can be encoded")
|
265
|
+
end
|
266
|
+
i += 1
|
267
|
+
end
|
268
|
+
# Return string of 16-bit codepoints.
|
269
|
+
data = codepoints.pack("n*")
|
270
|
+
result = (CFBinaryPlistMarkerUnicode16String |
|
271
|
+
(codepoints.length < 15 ? codepoints.length : 0xf)).chr
|
272
|
+
result += binary_plist_obj(codepoints.length) if codepoints.length >= 15
|
273
|
+
result += data
|
274
|
+
return result
|
275
|
+
end
|
276
|
+
when Float
|
277
|
+
return (CFBinaryPlistMarkerReal | 3).chr + [obj].pack("G")
|
278
|
+
when Integer
|
279
|
+
nbytes = min_byte_size(obj)
|
280
|
+
size_bits = { 1 => 0, 2 => 1, 4 => 2, 8 => 3, 16 => 4 }[nbytes]
|
281
|
+
return (CFBinaryPlistMarkerInt | size_bits).chr + pack_int(obj, nbytes)
|
282
|
+
when TrueClass
|
283
|
+
return CFBinaryPlistMarkerTrue.chr
|
284
|
+
when FalseClass
|
285
|
+
return CFBinaryPlistMarkerFalse.chr
|
286
|
+
when Time
|
287
|
+
return CFBinaryPlistMarkerDate.chr +
|
288
|
+
[obj.to_f - NSTimeIntervalSince1970].pack("G")
|
289
|
+
when IO, StringIO
|
290
|
+
obj.rewind
|
291
|
+
return binary_plist_data(obj.read)
|
292
|
+
when Array
|
293
|
+
# Must be an array of object references as returned by flatten_collection.
|
294
|
+
result = (CFBinaryPlistMarkerArray | (obj.length < 15 ? obj.length : 0xf)).chr
|
295
|
+
result += binary_plist_obj(obj.length) if obj.length >= 15
|
296
|
+
result += obj.collect! { |i| pack_int(i, ref_byte_size) }.join
|
297
|
+
when Set
|
298
|
+
# Must be a set of object references as returned by flatten_collection.
|
299
|
+
result = (CFBinaryPlistMarkerSet | (obj.length < 15 ? obj.length : 0xf)).chr
|
300
|
+
result += binary_plist_obj(obj.length) if obj.length >= 15
|
301
|
+
result += obj.to_a.collect! { |i| pack_int(i, ref_byte_size) }.join
|
302
|
+
when Hash
|
303
|
+
# Must be a table of object references as returned by flatten_collection.
|
304
|
+
result = (CFBinaryPlistMarkerDict | (obj.length < 15 ? obj.length : 0xf)).chr
|
305
|
+
result += binary_plist_obj(obj.length) if obj.length >= 15
|
306
|
+
result += obj.keys.collect! { |i| pack_int(i, ref_byte_size) }.join
|
307
|
+
result += obj.values.collect! { |i| pack_int(i, ref_byte_size) }.join
|
308
|
+
else
|
309
|
+
return binary_plist_data(Marshal.dump(obj))
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def self.decode_binary_plist_obj(plist, offset, ref_byte_size)
|
314
|
+
case plist[offset]
|
315
|
+
when CFBinaryPlistMarkerASCIIString..(CFBinaryPlistMarkerASCIIString | 0xf)
|
316
|
+
length, offset = decode_length(plist, offset)
|
317
|
+
return plist[offset, length]
|
318
|
+
when CFBinaryPlistMarkerUnicode16String..(CFBinaryPlistMarkerUnicode16String | 0xf)
|
319
|
+
length, offset = decode_length(plist, offset)
|
320
|
+
codepoints = plist[offset, length * 2].unpack("n*")
|
321
|
+
str = ""
|
322
|
+
codepoints.each do |codepoint|
|
323
|
+
if codepoint <= 0x7f
|
324
|
+
ch = ' '
|
325
|
+
ch[0] = to_i
|
326
|
+
elsif codepoint <= 0x7ff
|
327
|
+
ch = ' '
|
328
|
+
ch[0] = ((codepoint & 0x7c0) >> 6) | 0xc0
|
329
|
+
ch[1] = codepoint & 0x3f | 0x80
|
330
|
+
else
|
331
|
+
ch = ' '
|
332
|
+
ch[0] = ((codepoint & 0xf000) >> 12) | 0xe0
|
333
|
+
ch[1] = ((codepoint & 0xfc0) >> 6) | 0x80
|
334
|
+
ch[2] = codepoint & 0x3f | 0x80
|
335
|
+
end
|
336
|
+
str << ch
|
337
|
+
end
|
338
|
+
return str
|
339
|
+
when CFBinaryPlistMarkerReal | 3
|
340
|
+
return plist[offset+1, 8].unpack("G").first
|
341
|
+
when CFBinaryPlistMarkerInt..(CFBinaryPlistMarkerInt | 0xf)
|
342
|
+
num_bytes = 2 ** (plist[offset] & 0xf)
|
343
|
+
return unpack_int(plist[offset+1, num_bytes])
|
344
|
+
when CFBinaryPlistMarkerTrue
|
345
|
+
return true
|
346
|
+
when CFBinaryPlistMarkerFalse
|
347
|
+
return false
|
348
|
+
when CFBinaryPlistMarkerDate
|
349
|
+
secs = plist[offset+1, 8].unpack("G").first + NSTimeIntervalSince1970
|
350
|
+
return Time.at(secs)
|
351
|
+
when CFBinaryPlistMarkerData..(CFBinaryPlistMarkerData | 0xf)
|
352
|
+
length, offset = decode_length(plist, offset)
|
353
|
+
return StringIO.new(plist[offset, length])
|
354
|
+
when CFBinaryPlistMarkerArray..(CFBinaryPlistMarkerArray | 0xf)
|
355
|
+
ary = []
|
356
|
+
length, offset = decode_length(plist, offset)
|
357
|
+
length.times do
|
358
|
+
ary << unpack_int(plist[offset, ref_byte_size])
|
359
|
+
offset += ref_byte_size
|
360
|
+
end
|
361
|
+
return ary
|
362
|
+
when CFBinaryPlistMarkerDict..(CFBinaryPlistMarkerDict | 0xf)
|
363
|
+
hsh = {}
|
364
|
+
keys = []
|
365
|
+
length, offset = decode_length(plist, offset)
|
366
|
+
length.times do
|
367
|
+
keys << unpack_int(plist[offset, ref_byte_size])
|
368
|
+
offset += ref_byte_size
|
369
|
+
end
|
370
|
+
length.times do |i|
|
371
|
+
hsh[keys[i]] = unpack_int(plist[offset, ref_byte_size])
|
372
|
+
offset += ref_byte_size
|
373
|
+
end
|
374
|
+
return hsh
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# Returns a binary property list fragment that represents a data object
|
379
|
+
# with the contents of the string +data+. A Cocoa application would decode
|
380
|
+
# this fragment as NSData. Like binary_plist_obj, the value returned by
|
381
|
+
# this method is not usable by itself; it is only useful as part of a
|
382
|
+
# complete binary property list with a header, offset table, and trailer.
|
383
|
+
def self.binary_plist_data(data)
|
384
|
+
result = (CFBinaryPlistMarkerData |
|
385
|
+
(data.length < 15 ? data.length : 0xf)).chr
|
386
|
+
result += binary_plist_obj(data.length) if data.length > 15
|
387
|
+
result += data
|
388
|
+
return result
|
389
|
+
end
|
390
|
+
|
391
|
+
# Determines the minimum number of bytes that is a power of two and can
|
392
|
+
# represent the integer +i+. Raises a RangeError if the number of bytes
|
393
|
+
# exceeds 16. Note that the property list format considers integers of 1,
|
394
|
+
# 2, and 4 bytes to be unsigned, while 8- and 16-byte integers are signed;
|
395
|
+
# thus negative integers will always require at least 8 bytes of storage.
|
396
|
+
def self.min_byte_size(i)
|
397
|
+
if i < 0
|
398
|
+
i = i.abs - 1
|
399
|
+
else
|
400
|
+
if i <= 0xff
|
401
|
+
return 1
|
402
|
+
elsif i <= 0xffff
|
403
|
+
return 2
|
404
|
+
elsif i <= 0xffffffff
|
405
|
+
return 4
|
406
|
+
end
|
407
|
+
end
|
408
|
+
if i <= 0x7fffffffffffffff
|
409
|
+
return 8
|
410
|
+
elsif i <= 0x7fffffffffffffffffffffffffffffff
|
411
|
+
return 16
|
412
|
+
end
|
413
|
+
raise(RangeError, "integer too big - exceeds 128 bits")
|
414
|
+
end
|
415
|
+
|
416
|
+
# Packs an integer +i+ into its binary representation in the specified
|
417
|
+
# number of bytes. Byte order is big-endian. Negative integers cannot be
|
418
|
+
# stored in 1, 2, or 4 bytes.
|
419
|
+
def self.pack_int(i, num_bytes)
|
420
|
+
if i < 0 && num_bytes < 8
|
421
|
+
raise(ArgumentError, "negative integers require 8 or 16 bytes of storage")
|
422
|
+
end
|
423
|
+
case num_bytes
|
424
|
+
when 1
|
425
|
+
[i].pack("c")
|
426
|
+
when 2
|
427
|
+
[i].pack("n")
|
428
|
+
when 4
|
429
|
+
[i].pack("N")
|
430
|
+
when 8
|
431
|
+
[(i >> 32) & 0xffffffff, i & 0xffffffff].pack("NN")
|
432
|
+
when 16
|
433
|
+
[i >> 96, (i >> 64) & 0xffffffff, (i >> 32) & 0xffffffff,
|
434
|
+
i & 0xffffffff].pack("NNNN")
|
435
|
+
else
|
436
|
+
raise(ArgumentError, "num_bytes must be 1, 2, 4, 8, or 16")
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
def self.combine_ints(num_bits, *ints)
|
441
|
+
i = ints.pop
|
442
|
+
shift_bits = num_bits
|
443
|
+
ints.reverse.each do |i_part|
|
444
|
+
i += i_part << shift_bits
|
445
|
+
shift_bits += num_bits
|
446
|
+
end
|
447
|
+
return i
|
448
|
+
end
|
449
|
+
|
450
|
+
def self.offset_for_index(plist, table_addr, offset_byte_size, index)
|
451
|
+
offset = plist[table_addr + index * offset_byte_size, offset_byte_size]
|
452
|
+
unpack_int(offset)
|
453
|
+
end
|
454
|
+
|
455
|
+
def self.unpack_int(s)
|
456
|
+
case s.length
|
457
|
+
when 1
|
458
|
+
s.unpack("C").first
|
459
|
+
when 2
|
460
|
+
s.unpack("n").first
|
461
|
+
when 4
|
462
|
+
s.unpack("N").first
|
463
|
+
when 8
|
464
|
+
i = combine_ints(32, *(s.unpack("NN")))
|
465
|
+
(i & 0x80000000_00000000 == 0) ?
|
466
|
+
i :
|
467
|
+
-(i ^ 0xffffffff_ffffffff) - 1
|
468
|
+
when 16
|
469
|
+
i = combine_ints(32, *(s.unpack("NNNN")))
|
470
|
+
(i & 0x80000000_00000000_00000000_00000000 == 0) ?
|
471
|
+
i :
|
472
|
+
-(i ^ 0xffffffff_ffffffff_ffffffff_ffffffff) - 1
|
473
|
+
else
|
474
|
+
raise(ArgumentError, "length must be 1, 2, 4, 8, or 16 bytes")
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def self.decode_length(plist, offset)
|
479
|
+
if plist[offset] & 0xf == 0xf
|
480
|
+
offset += 1
|
481
|
+
length = decode_binary_plist_obj(plist, offset, 0)
|
482
|
+
offset += min_byte_size(length) + 1
|
483
|
+
return length, offset
|
484
|
+
else
|
485
|
+
return (plist[offset] & 0xf), (offset + 1)
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
data/lib/ipa_reader.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
module IpaReader
|
3
|
+
|
4
|
+
# :stopdoc:
|
5
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
6
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
7
|
+
# :startdoc:
|
8
|
+
|
9
|
+
# Returns the version string for the library.
|
10
|
+
#
|
11
|
+
def self.version
|
12
|
+
@version ||= File.read(path('version.txt')).strip
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the library path for the module. If any arguments are given,
|
16
|
+
# they will be joined to the end of the libray path using
|
17
|
+
# <tt>File.join</tt>.
|
18
|
+
#
|
19
|
+
def self.libpath( *args, &block )
|
20
|
+
rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
21
|
+
if block
|
22
|
+
begin
|
23
|
+
$LOAD_PATH.unshift LIBPATH
|
24
|
+
rv = block.call
|
25
|
+
ensure
|
26
|
+
$LOAD_PATH.shift
|
27
|
+
end
|
28
|
+
end
|
29
|
+
return rv
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the lpath for the module. If any arguments are given,
|
33
|
+
# they will be joined to the end of the path using
|
34
|
+
# <tt>File.join</tt>.
|
35
|
+
#
|
36
|
+
def self.path( *args, &block )
|
37
|
+
rv = args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
38
|
+
if block
|
39
|
+
begin
|
40
|
+
$LOAD_PATH.unshift PATH
|
41
|
+
rv = block.call
|
42
|
+
ensure
|
43
|
+
$LOAD_PATH.shift
|
44
|
+
end
|
45
|
+
end
|
46
|
+
return rv
|
47
|
+
end
|
48
|
+
|
49
|
+
# Utility method used to require all files ending in .rb that lie in the
|
50
|
+
# directory below this file that has the same name as the filename passed
|
51
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
52
|
+
# the _filename_ does not have to be equivalent to the directory.
|
53
|
+
#
|
54
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
55
|
+
dir ||= ::File.basename(fname, '.*')
|
56
|
+
search_me = ::File.expand_path(
|
57
|
+
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
58
|
+
|
59
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
60
|
+
end
|
61
|
+
|
62
|
+
end # module IpaReader
|
63
|
+
|
64
|
+
IpaReader.require_all_libs_relative_to(__FILE__)
|
65
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
require File.expand_path(
|
3
|
+
File.join(File.dirname(__FILE__), %w[.. lib ipa_reader]))
|
4
|
+
|
5
|
+
Spec::Runner.configure do |config|
|
6
|
+
# == Mock Framework
|
7
|
+
#
|
8
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
9
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
10
|
+
#
|
11
|
+
# config.mock_with :mocha
|
12
|
+
# config.mock_with :flexmock
|
13
|
+
# config.mock_with :rr
|
14
|
+
end
|
15
|
+
|
data/test/MultiG.ipa
ADDED
Binary file
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/ipa_reader'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class IpaReaderTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@ipa_file = IpaReader::IpaFile.new(File.dirname(__FILE__) + '/MultiG.ipa')
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_parse
|
10
|
+
assert(@ipa_file.plist.keys.count > 0)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_version
|
14
|
+
assert_equal(@ipa_file.version, "1.2.2.4")
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_name
|
18
|
+
assert_equal(@ipa_file.name, "MultiG")
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_target_os_version
|
22
|
+
assert_equal(@ipa_file.target_os_version, "4.1")
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_minimum_os_version
|
26
|
+
assert_equal(@ipa_file.minimum_os_version, "3.1")
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_url_schemes
|
30
|
+
assert_equal(@ipa_file.url_schemes, [])
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_bundle_identifier
|
34
|
+
assert_equal("com.dcrails.multig", @ipa_file.bundle_identifier)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_icon_prerendered
|
38
|
+
assert_equal(false, @ipa_file.icon_prerendered)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
end
|
data/version.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ipa_reader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 1
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 5
|
9
|
+
version: "0.5"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Nicholas Schlueter
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-09-30 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: bones
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 25
|
29
|
+
segments:
|
30
|
+
- 3
|
31
|
+
- 4
|
32
|
+
- 7
|
33
|
+
version: 3.4.7
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
description: Reads metadata form iPhone Package Archive Files (ipa).
|
37
|
+
email: schlueter@gmail.com
|
38
|
+
executables:
|
39
|
+
- ipa_reader
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- History.txt
|
44
|
+
- README.txt
|
45
|
+
- bin/ipa_reader
|
46
|
+
- version.txt
|
47
|
+
files:
|
48
|
+
- .rvmrc
|
49
|
+
- History.txt
|
50
|
+
- README.txt
|
51
|
+
- Rakefile
|
52
|
+
- bin/ipa_reader
|
53
|
+
- lib/ipa_reader.rb
|
54
|
+
- lib/ipa_reader/ipa_file.rb
|
55
|
+
- lib/ipa_reader/plist_binary.rb
|
56
|
+
- spec/ipa_reader_spec.rb
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
- test/MultiG.ipa
|
59
|
+
- test/test_ipa_reader.rb
|
60
|
+
- version.txt
|
61
|
+
has_rdoc: true
|
62
|
+
homepage: http://github.com/schlueter/Ipa-Reader
|
63
|
+
licenses: []
|
64
|
+
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options:
|
67
|
+
- --main
|
68
|
+
- README.txt
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project: ipa_reader
|
92
|
+
rubygems_version: 1.3.7
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Reads metadata form iPhone Package Archive Files (ipa)
|
96
|
+
test_files:
|
97
|
+
- test/test_ipa_reader.rb
|