ipa_reader 0.5
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/.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
|