cfbundle 0.1.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.
- checksums.yaml +7 -0
- data/lib/cfbundle/bundle.rb +255 -0
- data/lib/cfbundle/constants.rb +14 -0
- data/lib/cfbundle/localization.rb +66 -0
- data/lib/cfbundle/path_utils.rb +70 -0
- data/lib/cfbundle/plist.rb +37 -0
- data/lib/cfbundle/resource.rb +202 -0
- data/lib/cfbundle/storage/base.rb +69 -0
- data/lib/cfbundle/storage/file_system.rb +58 -0
- data/lib/cfbundle/storage/zip.rb +96 -0
- data/lib/cfbundle/storage_detection.rb +86 -0
- data/lib/cfbundle/version.rb +3 -0
- data/lib/cfbundle.rb +11 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 34fbdac8d95981c1ee22c2cb3b57a5f80d1cd975
|
4
|
+
data.tar.gz: 85f292ed9c14e869a69b2d01c3238765fe29f5ae
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d162902ed081d3c2cc34d108e0d881846333eec364c9daa34208477c3c66f0f4e1f08389e008272dea1487570f60562ce71f6a515218ccaa4169e5953d59c104
|
7
|
+
data.tar.gz: b8fbc74c34d59c06bf626f744517c74419773c4732fe01b49e9d5e44a8569bfb7219380e7d42c8fb414bd0f87762546d62940b96e9298b100923a1135e0ac143
|
@@ -0,0 +1,255 @@
|
|
1
|
+
require 'cfbundle/constants'
|
2
|
+
require 'cfbundle/localization'
|
3
|
+
require 'cfbundle/path_utils'
|
4
|
+
require 'cfbundle/plist'
|
5
|
+
require 'cfbundle/resource'
|
6
|
+
require 'cfbundle/storage_detection'
|
7
|
+
|
8
|
+
module CFBundle
|
9
|
+
# A Bundle is an abstraction of a bundle accessible by the program.
|
10
|
+
class Bundle
|
11
|
+
# Opens a bundle.
|
12
|
+
#
|
13
|
+
# With no associated block, {open} is a synonym for {initialize new}. If the
|
14
|
+
# optional code block is given, it will be passed the opened bundle as an
|
15
|
+
# argument and the Bundle object will automatically be closed when the block
|
16
|
+
# terminates. The value of the block will be returned from the method.
|
17
|
+
#
|
18
|
+
# @param file [Object] The file to open. See {initialize} for a description
|
19
|
+
# of the supported values.
|
20
|
+
# @yieldparam bundle [Bundle] The opened bundle. It is automatically closed
|
21
|
+
# when the block terminates.
|
22
|
+
# @return [Object] The return value of the block when a block if given.
|
23
|
+
# @return [Bundle] The opened bundle when no block is given.
|
24
|
+
def self.open(file)
|
25
|
+
bundle = new(file)
|
26
|
+
return bundle unless block_given?
|
27
|
+
begin
|
28
|
+
yield bundle
|
29
|
+
ensure
|
30
|
+
bundle.close
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Opens the bundle and returns a new Bundle object.
|
35
|
+
#
|
36
|
+
# A new {storage} is automatically created from the +file+ parameter:
|
37
|
+
# * If the file is a path to a bundle directory, a new {Storage::FileSystem}
|
38
|
+
# is created that references the bundle at the path.
|
39
|
+
# * If the file is an +IO+ or a path to a ZIP archive, a new {Storage::Zip}
|
40
|
+
# is created that references the bundle within the archive.
|
41
|
+
# The +rubyzip+ gem must be loaded and the archive is expected to contain
|
42
|
+
# a single bundle at its root (or inside a +Payload+ directory for +.ipa+
|
43
|
+
# archives).
|
44
|
+
# * If the file is a {Storage::Base}, it is used as the bundle's storage.
|
45
|
+
#
|
46
|
+
# You should send +#close+ when the bundle is no longer needed.
|
47
|
+
#
|
48
|
+
# Note that storages created from an exisiting +IO+ do not automatically
|
49
|
+
# close the file when the bundle is closed.
|
50
|
+
#
|
51
|
+
# @param file [Object] The file to open.
|
52
|
+
# @raise [ArgumentError] If the file cannot be opened.
|
53
|
+
def initialize(file)
|
54
|
+
@storage = StorageDetection.open(file)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Closes the bundle and its underlying storage.
|
58
|
+
# @return [void]
|
59
|
+
def close
|
60
|
+
@storage.close
|
61
|
+
end
|
62
|
+
|
63
|
+
# The abstract storage used by the bundle.
|
64
|
+
#
|
65
|
+
# The storage implements the methods that are used by Bundle to read the
|
66
|
+
# bundle from the underlying storage (ZIP archive or file system).
|
67
|
+
#
|
68
|
+
# @return [Storage::Base]
|
69
|
+
# @see Storage::FileSystem
|
70
|
+
# @see Storage::Zip
|
71
|
+
attr_reader :storage
|
72
|
+
|
73
|
+
# Returns the bundle's information property list hash.
|
74
|
+
# @return [Hash]
|
75
|
+
def info
|
76
|
+
@info ||= Plist.load_info_plist(self)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the bundle identifier from the bundle's information property list.
|
80
|
+
# @return [String, nil]
|
81
|
+
# @see INFO_KEY_BUNDLE_IDENTIFIER
|
82
|
+
def identifier
|
83
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_IDENTIFIER)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the bundle's build version number.
|
87
|
+
# @return [String, nil]
|
88
|
+
# @see INFO_KEY_BUNDLE_VERSION
|
89
|
+
def build_version
|
90
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_VERSION)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the bundle's release version number.
|
94
|
+
# @return [String, nil]
|
95
|
+
# @see INFO_KEY_BUNDLE_SHORT_VERSION_STRING
|
96
|
+
def release_version
|
97
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_SHORT_VERSION_STRING)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the bundle's OS Type code.
|
101
|
+
#
|
102
|
+
# The value for this key consists of a four-letter code.
|
103
|
+
# @return [String, nil]
|
104
|
+
# @see INFO_KEY_BUNDLE_PACKAGE_TYPE
|
105
|
+
# @see PACKAGE_TYPE_APPLICATION
|
106
|
+
# @see PACKAGE_TYPE_BUNDLE
|
107
|
+
# @see PACKAGE_TYPE_FRAMEWORK
|
108
|
+
def package_type
|
109
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_PACKAGE_TYPE)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns the short name of the bundle.
|
113
|
+
# @return [String, nil]
|
114
|
+
# @see INFO_KEY_BUNDLE_NAME
|
115
|
+
def name
|
116
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_NAME)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the user-visible name of the bundle.
|
120
|
+
# @return [String, nil]
|
121
|
+
# @see INFO_KEY_BUNDLE_DISPLAY_NAME
|
122
|
+
def display_name
|
123
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_DISPLAY_NAME)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns the name of the development language of the bundle.
|
127
|
+
# @return [String, nil]
|
128
|
+
# @see INFO_KEY_BUNDLE_DEVELOPMENT_REGION
|
129
|
+
def development_localization
|
130
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_DEVELOPMENT_REGION)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns the name of the bundle's executable file.
|
134
|
+
# @return [String, nil]
|
135
|
+
# @see INFO_KEY_BUNDLE_EXECUTABLE
|
136
|
+
def executable_name
|
137
|
+
info_string(CFBundle::INFO_KEY_BUNDLE_EXECUTABLE)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns the path of the bundle's executable file.
|
141
|
+
#
|
142
|
+
# The executable's path is relative to the bundle's path.
|
143
|
+
# @return [String, nil]
|
144
|
+
# @see INFO_KEY_BUNDLE_EXECUTABLE
|
145
|
+
def executable_path
|
146
|
+
@executable_path ||=
|
147
|
+
executable_name && lookup_executable_path(executable_name)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the path of the bundle's subdirectory that contains its resources.
|
151
|
+
#
|
152
|
+
# The path is relative to the bundle's path. For iOS application bundles,
|
153
|
+
# as the resources directory is the bundle, this method returns a single dot
|
154
|
+
# (+.+).
|
155
|
+
# @return [String]
|
156
|
+
# @see Resource
|
157
|
+
def resources_directory
|
158
|
+
case layout_version
|
159
|
+
when 0 then 'Resources'
|
160
|
+
when 2 then 'Contents/Resources'
|
161
|
+
when 3 then '.'
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns a list of all the localizations contained in the bundle.
|
166
|
+
# @return [Array]
|
167
|
+
def localizations
|
168
|
+
@localizations ||= Localization.localizations_in(self)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns an ordered list of preferred localizations contained in the
|
172
|
+
# bundle.
|
173
|
+
# @param preferred_languages [Array] An array of strings (or symbols)
|
174
|
+
# corresponding to a user's preferred languages.
|
175
|
+
# @return [Array]
|
176
|
+
def preferred_localizations(preferred_languages)
|
177
|
+
Localization.preferred_localizations(localizations, preferred_languages)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the first {Resource} object that matches the specified parameters.
|
181
|
+
#
|
182
|
+
# @param name [String?] The name to match or +nil+ to match any name.
|
183
|
+
# @param extension [String?] The extension to match or +nil+ to match any
|
184
|
+
# extension.
|
185
|
+
# @param subdirectory [String?] The name of the bundle subdirectory
|
186
|
+
# search.
|
187
|
+
# @param localization [String?, Symbol?] A language identifier to restrict
|
188
|
+
# the search to a specific localization.
|
189
|
+
# @param preferred_languages [Array] An array of strings (or symbols)
|
190
|
+
# corresponding to a user's preferred languages.
|
191
|
+
# @param product [String?] The product to match or +nil+ to match any
|
192
|
+
# product.
|
193
|
+
# @return [Resource?]
|
194
|
+
# @see Resource.foreach
|
195
|
+
def find_resource(name, extension: nil, subdirectory: nil,
|
196
|
+
localization: nil, preferred_languages: [], product: nil)
|
197
|
+
Resource.foreach(
|
198
|
+
self, name,
|
199
|
+
extension: extension, subdirectory: subdirectory,
|
200
|
+
localization: localization, preferred_languages: preferred_languages,
|
201
|
+
product: product
|
202
|
+
).first
|
203
|
+
end
|
204
|
+
|
205
|
+
# Returns all the {Resource} objects that matches the specified parameters.
|
206
|
+
#
|
207
|
+
# @param name [String?] The name to match or +nil+ to match any name.
|
208
|
+
# @param extension [String?] The extension to match or +nil+ to match any
|
209
|
+
# extension.
|
210
|
+
# @param subdirectory [String?] The name of the bundle subdirectory to
|
211
|
+
# search.
|
212
|
+
# @param localization [String?, Array] The language identifier for the
|
213
|
+
# localization.
|
214
|
+
# @param preferred_languages [Array] An array of strings (or symbols)
|
215
|
+
# corresponding to a user's preferred languages.
|
216
|
+
# @param product [String?] The product to match or +nil+ to match any
|
217
|
+
# product.
|
218
|
+
# @return [Array] An array of {Resource} objects.
|
219
|
+
# @see Resource.foreach
|
220
|
+
def find_resources(name, extension: nil, subdirectory: nil,
|
221
|
+
localization: nil, preferred_languages: [], product: nil)
|
222
|
+
Resource.foreach(
|
223
|
+
self, name,
|
224
|
+
extension: extension, subdirectory: subdirectory,
|
225
|
+
localization: localization, preferred_languages: preferred_languages,
|
226
|
+
product: product
|
227
|
+
).to_a
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def layout_version
|
233
|
+
@layout_version ||= detect_layout_version
|
234
|
+
end
|
235
|
+
|
236
|
+
def detect_layout_version
|
237
|
+
return 2 if storage.directory? 'Contents'
|
238
|
+
return 0 if storage.directory? 'Resources'
|
239
|
+
3
|
240
|
+
end
|
241
|
+
|
242
|
+
def info_string(key)
|
243
|
+
value = info[key.to_s]
|
244
|
+
value.to_s unless value.nil?
|
245
|
+
end
|
246
|
+
|
247
|
+
def lookup_executable_path(name)
|
248
|
+
root = layout_version == 2 ? 'Contents' : '.'
|
249
|
+
path = PathUtils.join(root, 'MacOS', name)
|
250
|
+
return path if storage.file? path
|
251
|
+
path = PathUtils.join(root, name)
|
252
|
+
return path if storage.file? path
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module CFBundle
|
2
|
+
INFO_KEY_BUNDLE_DEVELOPMENT_REGION = 'CFBundleDevelopmentRegion'.freeze
|
3
|
+
INFO_KEY_BUNDLE_DISPLAY_NAME = 'CFBundleDisplayName'.freeze
|
4
|
+
INFO_KEY_BUNDLE_EXECUTABLE = 'CFBundleExecutable'.freeze
|
5
|
+
INFO_KEY_BUNDLE_IDENTIFIER = 'CFBundleIdentifier'.freeze
|
6
|
+
INFO_KEY_BUNDLE_NAME = 'CFBundleName'.freeze
|
7
|
+
INFO_KEY_BUNDLE_PACKAGE_TYPE = 'CFBundlePackageType'.freeze
|
8
|
+
INFO_KEY_BUNDLE_SHORT_VERSION_STRING = 'CFBundleShortVersionString'.freeze
|
9
|
+
INFO_KEY_BUNDLE_VERSION = 'CFBundleVersion'.freeze
|
10
|
+
|
11
|
+
PACKAGE_TYPE_APPLICATION = 'APPL'.freeze
|
12
|
+
PACKAGE_TYPE_BUNDLE = 'BNDL'.freeze
|
13
|
+
PACKAGE_TYPE_FRAMEWORK = 'FMWK'.freeze
|
14
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module CFBundle
|
2
|
+
# @private
|
3
|
+
#
|
4
|
+
# Utility methods to perform localization.
|
5
|
+
module Localization
|
6
|
+
# The file extension of localization directories.
|
7
|
+
FILE_EXTENSION = '.lproj'.freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Returns all the localizations contained in a bundle.
|
11
|
+
# @param bundle [Bundle] The bundle to search.
|
12
|
+
# @return [Array]
|
13
|
+
def localizations_in(bundle)
|
14
|
+
return [] unless bundle.storage.directory?(bundle.resources_directory)
|
15
|
+
bundle.storage
|
16
|
+
.foreach(bundle.resources_directory)
|
17
|
+
.select { |path| File.extname(path) == FILE_EXTENSION }
|
18
|
+
.map { |path| File.basename(path, FILE_EXTENSION) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns an ordered list of preferred localizations contained in a
|
22
|
+
# bundle.
|
23
|
+
# @param localizations [Array] An array of localization identifiers.
|
24
|
+
# @param preferred_languages [Array] An array of strings (or symbols)
|
25
|
+
# corresponding to a user's preferred languages.
|
26
|
+
# @return [Array]
|
27
|
+
# @see Bundle#localizations
|
28
|
+
def preferred_localizations(localizations, preferred_languages)
|
29
|
+
preferred_languages.each do |language|
|
30
|
+
result = matching_localizations(localizations, language)
|
31
|
+
return result unless result.empty?
|
32
|
+
result = alternate_regional_localizations(localizations, language)
|
33
|
+
return result unless result.empty?
|
34
|
+
end
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def matching_localizations(localizations, language)
|
41
|
+
result = []
|
42
|
+
loop do
|
43
|
+
if localizations.include?(language.to_s) ||
|
44
|
+
localizations.include?(language.to_sym)
|
45
|
+
result << language
|
46
|
+
end
|
47
|
+
language = language.to_s.rpartition('-').first
|
48
|
+
break if language.empty?
|
49
|
+
end
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
def alternate_regional_localizations(localizations, language)
|
54
|
+
loop do
|
55
|
+
language = language.to_s.rpartition('-').first
|
56
|
+
return [] if language.empty?
|
57
|
+
prefix = language + '-'
|
58
|
+
match = localizations.find do |localization|
|
59
|
+
localization.start_with?(prefix)
|
60
|
+
end
|
61
|
+
return [match.to_s] if match
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module CFBundle
|
2
|
+
# @private
|
3
|
+
#
|
4
|
+
# Utility methods for manipulating paths
|
5
|
+
module PathUtils
|
6
|
+
class << self
|
7
|
+
# Returns a new path formed by joining the strings using
|
8
|
+
# +File::SEPARATOR+.
|
9
|
+
#
|
10
|
+
# The methods also makes sure to remove any trailing separator along with
|
11
|
+
# path components that are empty, nil or a single dot (+.+) in order to
|
12
|
+
# generate a canonical path.
|
13
|
+
#
|
14
|
+
# @param args [Array] An array of strings.
|
15
|
+
# @return [String]
|
16
|
+
def join(*args)
|
17
|
+
args.compact!
|
18
|
+
args.map! { |arg| arg.split(File::SEPARATOR) }
|
19
|
+
args.flatten!
|
20
|
+
args.reject! { |arg| arg == '.' }
|
21
|
+
return '.' if args.empty?
|
22
|
+
absolute = args.first == ''
|
23
|
+
args.reject! { |arg| arg == '' }
|
24
|
+
args.unshift('/') if absolute
|
25
|
+
File.join(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Splits the resource path into four components.
|
29
|
+
#
|
30
|
+
# The components are the resource's directory, name, product and
|
31
|
+
# extension. The product is either empty or starts with a tilde. The
|
32
|
+
# extension is either empty or starts with a dot.
|
33
|
+
# @param path [String] The path to the resource.
|
34
|
+
# @return [Array]
|
35
|
+
# @see join_resource
|
36
|
+
def split_resource(path)
|
37
|
+
directory = File.dirname(path)
|
38
|
+
extension = File.extname(path)
|
39
|
+
basename = File.basename(path, extension)
|
40
|
+
name, product = split_resource_name_and_product(basename)
|
41
|
+
[directory, name, product, extension]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a new path formed by joining the resource components.
|
45
|
+
# @param directory [String] The resource's directory.
|
46
|
+
# @param name [String] The resource's name.
|
47
|
+
# @param product [String] The resource's product. It should be empty or
|
48
|
+
# start with a tilde.
|
49
|
+
# @param extension [String] The resource's extension. It should be empty
|
50
|
+
# or start with a dot.
|
51
|
+
# @return [String]
|
52
|
+
# @see split_resource
|
53
|
+
def join_resource(directory, name, product, extension)
|
54
|
+
filename = [name, product, extension].join
|
55
|
+
join(directory, filename)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def split_resource_name_and_product(basename)
|
61
|
+
name, _, product = basename.rpartition('~')
|
62
|
+
if name.empty? || product.empty?
|
63
|
+
[basename, '']
|
64
|
+
else
|
65
|
+
[name, '~' + product]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'cfpropertylist'
|
2
|
+
|
3
|
+
module CFBundle
|
4
|
+
# @private
|
5
|
+
#
|
6
|
+
# Utility methods for manipulating Property list files.
|
7
|
+
module Plist
|
8
|
+
class << self
|
9
|
+
# Loads the +Info.plist+ file in a bundle.
|
10
|
+
# @param bundle [Bundle] The bundle to search.
|
11
|
+
# @return [Hash]
|
12
|
+
def load_info_plist(bundle)
|
13
|
+
load_plist(bundle, info_plist_path_in(bundle))
|
14
|
+
end
|
15
|
+
|
16
|
+
# Loads a Propery list file in a bundle.
|
17
|
+
# @param bundle [Bundle] The bundle to search.
|
18
|
+
# @param path [String] The path to the Property list file in the bundle.
|
19
|
+
# @return [Hash]
|
20
|
+
def load_plist(bundle, path)
|
21
|
+
data = bundle.storage.open(path, &:read)
|
22
|
+
plist = CFPropertyList::List.new(data: data)
|
23
|
+
CFPropertyList.native_types(plist.value)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def info_plist_path_in(bundle)
|
29
|
+
case bundle.send :layout_version
|
30
|
+
when 0 then 'Resources/Info.plist'
|
31
|
+
when 2 then 'Contents/Info.plist'
|
32
|
+
when 3 then 'Info.plist'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'cfbundle/path_utils'
|
2
|
+
|
3
|
+
module CFBundle
|
4
|
+
# A {Resource} is an abstraction of file contained within a {Bundle}.
|
5
|
+
class Resource
|
6
|
+
# Returns the resource's enclosing bundle.
|
7
|
+
# @return [Bundle]
|
8
|
+
attr_reader :bundle
|
9
|
+
|
10
|
+
# Returns the path to the resource within the bundle.
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :path
|
13
|
+
|
14
|
+
# @param bundle [Bundle] The resource's enclosing bundle.
|
15
|
+
# @param path [String] The path of the resource within the bundle.
|
16
|
+
def initialize(bundle, path)
|
17
|
+
@bundle = bundle
|
18
|
+
@path = path
|
19
|
+
@directory, @name, @product, @extension = PathUtils.split_resource(path)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Opens the resource for reading.
|
23
|
+
#
|
24
|
+
# With no associated block, the method returns an IO. If the optional block
|
25
|
+
# is given, it will be passed the opened file and the file will
|
26
|
+
# automatically be closed when the block terminates.
|
27
|
+
# @yieldparam file [IO] The opened file. It is automatically closed when the
|
28
|
+
# block terminates.
|
29
|
+
# @return [Object] The return value to the block when a block is given.
|
30
|
+
# @return [IO] The opened file. When no block is given.
|
31
|
+
def open(&block)
|
32
|
+
bundle.storage.open(path, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Enumerates the resources in a bundle that match the specified parameters.
|
36
|
+
#
|
37
|
+
# @param bundle [Bundle] The bundle that contains the resources.
|
38
|
+
# @param name [String?] The name to match or +nil+ to match any name.
|
39
|
+
# @param extension [String?] The extension to match or +nil+ to match any
|
40
|
+
# extension.
|
41
|
+
# @param localization [String?, Symbol?] A language identifier to restrict
|
42
|
+
# the search to a specific localization.
|
43
|
+
# @param preferred_languages [Array] An array of strings (or symbols)
|
44
|
+
# corresponding to a user's preferred languages.
|
45
|
+
# @param product [String?] The product to match or +nil+ to match any
|
46
|
+
# product.
|
47
|
+
# @yieldparam resource [Resource]
|
48
|
+
# @return [nil] When a block is given.
|
49
|
+
# @return [Enumerator] When no block is given.
|
50
|
+
def self.foreach(bundle, name, extension: nil, subdirectory: nil,
|
51
|
+
localization: nil, preferred_languages: [], product: nil,
|
52
|
+
&block)
|
53
|
+
enumerator = ::Enumerator.new do |y|
|
54
|
+
enumerator = Enumerator.new(bundle, subdirectory, localization,
|
55
|
+
preferred_languages)
|
56
|
+
predicate = Predicate.new(name, extension, product)
|
57
|
+
loop do
|
58
|
+
resource = enumerator.next
|
59
|
+
y << resource if resource.send(:match?, predicate)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
enumerator.each(&block)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def match?(predicate)
|
68
|
+
return false unless name_match?(predicate) &&
|
69
|
+
extension_match?(predicate) &&
|
70
|
+
product_match?(predicate)
|
71
|
+
predicate.uniq?(@name, @extension)
|
72
|
+
end
|
73
|
+
|
74
|
+
def name_match?(predicate)
|
75
|
+
predicate.name.nil? || @name == predicate.name
|
76
|
+
end
|
77
|
+
|
78
|
+
def extension_match?(predicate)
|
79
|
+
predicate.extension.nil? || @extension == predicate.extension
|
80
|
+
end
|
81
|
+
|
82
|
+
def product_match?(predicate)
|
83
|
+
return true if @product == predicate.product
|
84
|
+
return false unless @product.empty?
|
85
|
+
!bundle.storage.exist?(path_with_product(predicate.product))
|
86
|
+
end
|
87
|
+
|
88
|
+
def path_with_product(product)
|
89
|
+
PathUtils.join_resource(@directory, @name, product, @extension)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @private
|
93
|
+
#
|
94
|
+
# Performs the enumeration of a bundle's resources.
|
95
|
+
class Enumerator
|
96
|
+
# @param bundle [Bundle] The bundle that contains the resources.
|
97
|
+
# @param subdirectory [String?] The name of the bundle subdirectory to
|
98
|
+
# search.
|
99
|
+
# @param localization [String?, Symbol?] A language identifier to restrict
|
100
|
+
# the search to a specific localization.
|
101
|
+
# @param preferred_languages [Array] An array of strings (or symbols)
|
102
|
+
# corresponding to a user's preferred languages.
|
103
|
+
def initialize(bundle, subdirectory, localization, preferred_languages)
|
104
|
+
@bundle = bundle
|
105
|
+
@directory = PathUtils.join(bundle.resources_directory, subdirectory)
|
106
|
+
@localizations = localizations_for(bundle, localization,
|
107
|
+
preferred_languages)
|
108
|
+
@enumerator = [].to_enum
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the next resource in the bundle.
|
112
|
+
#
|
113
|
+
# @return [Resource]
|
114
|
+
# @raise [StopIteration]
|
115
|
+
def next
|
116
|
+
Resource.new(@bundle, @enumerator.next)
|
117
|
+
rescue StopIteration
|
118
|
+
@enumerator = next_enumerator
|
119
|
+
retry
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def next_enumerator
|
125
|
+
loop do
|
126
|
+
break if @localizations.empty?
|
127
|
+
localization = @localizations.shift
|
128
|
+
directory = localized_directory_for(localization)
|
129
|
+
next unless @bundle.storage.directory?(directory)
|
130
|
+
return @bundle.storage.foreach(directory)
|
131
|
+
end
|
132
|
+
raise StopIteration
|
133
|
+
end
|
134
|
+
|
135
|
+
def localized_directory_for(localization)
|
136
|
+
return @directory unless localization
|
137
|
+
PathUtils.join(@directory, localization + '.lproj')
|
138
|
+
end
|
139
|
+
|
140
|
+
def localizations_for(bundle, localization, preferred_languages)
|
141
|
+
return [nil, localization.to_s] if localization
|
142
|
+
[
|
143
|
+
nil,
|
144
|
+
*bundle.preferred_localizations(preferred_languages || []),
|
145
|
+
bundle.development_localization
|
146
|
+
].uniq
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# @private
|
151
|
+
#
|
152
|
+
# Stores the parameters to select the matching resources while enumerating
|
153
|
+
# the resources of a bundle.
|
154
|
+
class Predicate
|
155
|
+
# Returns the name to match or +nil+ to match any name.
|
156
|
+
# @return [String?]
|
157
|
+
attr_reader :name
|
158
|
+
|
159
|
+
# Returns the extension to match or +nil+ to match any extension.
|
160
|
+
# @return [String?]
|
161
|
+
attr_reader :extension
|
162
|
+
|
163
|
+
# Returns the product to match.
|
164
|
+
# @return [String]
|
165
|
+
attr_reader :product
|
166
|
+
|
167
|
+
# @param name [String?] The name to match or +nil+ to match any name.
|
168
|
+
# @param extension [String?] The extension to match or +nil+ to match any
|
169
|
+
# extension.
|
170
|
+
# @param product [String?] The product to match.
|
171
|
+
def initialize(name, extension, product)
|
172
|
+
@name = name
|
173
|
+
@extension = extension_for(extension)
|
174
|
+
@product = product_for(product)
|
175
|
+
@keys = Set.new
|
176
|
+
end
|
177
|
+
|
178
|
+
# Ensures the given name and extension are unique during the enumeration.
|
179
|
+
#
|
180
|
+
# @param name [String] The resource name.
|
181
|
+
# @param extension [String] The resource extension.
|
182
|
+
def uniq?(name, extension)
|
183
|
+
key = [name, extension].join
|
184
|
+
return false if @keys.include?(key)
|
185
|
+
@keys << key
|
186
|
+
true
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def extension_for(extension)
|
192
|
+
return extension if extension.nil? || extension.empty?
|
193
|
+
extension.start_with?('.') ? extension : '.' + extension
|
194
|
+
end
|
195
|
+
|
196
|
+
def product_for(product)
|
197
|
+
return '' if product.nil? || product.empty?
|
198
|
+
product.start_with?('~') ? product : '~' + product
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module CFBundle
|
2
|
+
module Storage
|
3
|
+
# The {Storage::Base} class defines the methods required to access a
|
4
|
+
# bundle's underlying storage.
|
5
|
+
#
|
6
|
+
# Most of the time, you don't need to concern youself with storages as
|
7
|
+
# {CFBundle::Bundle.open} and {CFBundle::Bundle#initialize}
|
8
|
+
# automatically detect and instantiate the appropriate storage.
|
9
|
+
class Base
|
10
|
+
# Returns whether a given path exists within the storage.
|
11
|
+
#
|
12
|
+
# @param path [String] The path of a file or directory, relative to the
|
13
|
+
# storage.
|
14
|
+
def exist?(path)
|
15
|
+
# :nocov:
|
16
|
+
false
|
17
|
+
# :nocov:
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns whether a given file exists within the storage.
|
21
|
+
#
|
22
|
+
# @param path [String] The path of a file, relative to the storage.
|
23
|
+
def file?(path)
|
24
|
+
# :nocov:
|
25
|
+
false
|
26
|
+
# :nocov:
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns whether a given directory exists within the storage.
|
30
|
+
#
|
31
|
+
# @param path [String] The path of a directory, relative to the storage.
|
32
|
+
def directory?(path)
|
33
|
+
# :nocov:
|
34
|
+
false
|
35
|
+
# :nocov:
|
36
|
+
end
|
37
|
+
|
38
|
+
# Opens a file for reading in the storage.
|
39
|
+
#
|
40
|
+
# @param path [String] The path of the file to open.
|
41
|
+
# @yieldparam file [IO] The opened file. It is automatically closed when
|
42
|
+
# the block terminates.
|
43
|
+
# @return [Object] The return value of the block when a block if given.
|
44
|
+
# @return [IO] The opened file when no block is given.
|
45
|
+
def open(path, &block)
|
46
|
+
# :nocov:
|
47
|
+
raise(Errno::ENOENT, path)
|
48
|
+
# :nocov:
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns an enumerator that enumerates the files contained in a
|
52
|
+
# directory.
|
53
|
+
#
|
54
|
+
# @param path [String] The path to the directory to enumerate.
|
55
|
+
# @return [Enumerator]
|
56
|
+
def foreach(path)
|
57
|
+
# :nocov:
|
58
|
+
raise(Errno::ENOENT, path)
|
59
|
+
# :nocov:
|
60
|
+
end
|
61
|
+
|
62
|
+
# Invoked when the storage is no longer needed.
|
63
|
+
#
|
64
|
+
# The default implementation does nothing.
|
65
|
+
# @return [void]
|
66
|
+
def close; end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'cfbundle/path_utils'
|
2
|
+
require 'cfbundle/storage/base'
|
3
|
+
|
4
|
+
module CFBundle
|
5
|
+
module Storage
|
6
|
+
# A bundle storage that reads from the file system.
|
7
|
+
class FileSystem < Base
|
8
|
+
# @param path [String] The path of the bundle on the file system.
|
9
|
+
def initialize(path)
|
10
|
+
@root = path
|
11
|
+
end
|
12
|
+
|
13
|
+
# (see Base#exist?)
|
14
|
+
def exist?(path)
|
15
|
+
find(path) != nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# (see Base#file?)
|
19
|
+
def file?(path)
|
20
|
+
entry = find(path)
|
21
|
+
!entry.nil? && File.file?(entry)
|
22
|
+
end
|
23
|
+
|
24
|
+
# (see Base#directory?)
|
25
|
+
def directory?(path)
|
26
|
+
entry = find(path)
|
27
|
+
!entry.nil? && File.directory?(entry)
|
28
|
+
end
|
29
|
+
|
30
|
+
# (see Base#open)
|
31
|
+
def open(path, &block)
|
32
|
+
File.open find!(path), &block
|
33
|
+
end
|
34
|
+
|
35
|
+
# (see Base#foreach)
|
36
|
+
def foreach(path)
|
37
|
+
Enumerator.new do |y|
|
38
|
+
base = Dir.foreach find!(path)
|
39
|
+
loop do
|
40
|
+
entry = base.next
|
41
|
+
y << PathUtils.join(path, entry) unless ['.', '..'].include?(entry)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def find(path)
|
49
|
+
entry = PathUtils.join(@root, path)
|
50
|
+
entry if File.exist? entry
|
51
|
+
end
|
52
|
+
|
53
|
+
def find!(path)
|
54
|
+
find(path) || raise(Errno::ENOENT, path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'cfbundle/path_utils'
|
2
|
+
require 'cfbundle/storage/base'
|
3
|
+
|
4
|
+
module CFBundle
|
5
|
+
module Storage
|
6
|
+
# A bundle storage that reads from a ZIP archive.
|
7
|
+
class Zip < Base
|
8
|
+
# @param zip [Zip::File] The Zip file containing the bundle.
|
9
|
+
# @param path [String] The path of the bundle within the Zip file.
|
10
|
+
# @param skip_close [Boolean] Whether the storage should skip closing
|
11
|
+
# the Zip file when receiving {#close}.
|
12
|
+
def initialize(zip, path, skip_close: false)
|
13
|
+
@zip = zip
|
14
|
+
@root = path
|
15
|
+
@skip_close = skip_close
|
16
|
+
end
|
17
|
+
|
18
|
+
# (see Base#exist?)
|
19
|
+
def exist?(path)
|
20
|
+
find(path) != nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# (see Base#file?)
|
24
|
+
def file?(path)
|
25
|
+
entry = find(path)
|
26
|
+
!entry.nil? && entry.file?
|
27
|
+
end
|
28
|
+
|
29
|
+
# (see Base#directory?)
|
30
|
+
def directory?(path)
|
31
|
+
entry = find(path)
|
32
|
+
!entry.nil? && entry.directory?
|
33
|
+
end
|
34
|
+
|
35
|
+
# (see Base#open)
|
36
|
+
def open(path, &block)
|
37
|
+
find!(path).get_input_stream(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# (see Base#foreach)
|
41
|
+
def foreach(path)
|
42
|
+
Enumerator.new do |y|
|
43
|
+
directory = find! path
|
44
|
+
base = @zip.entries.each
|
45
|
+
loop do
|
46
|
+
entry = base.next
|
47
|
+
next unless entry.parent_as_string == directory.name
|
48
|
+
y << PathUtils.join(path, File.basename(entry.name))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Invoked when the storage is no longer needed.
|
54
|
+
#
|
55
|
+
# This method closes the underlying Zip file unless the storage was
|
56
|
+
# initialized with +skip_close: true+.
|
57
|
+
# @return [void]
|
58
|
+
def close
|
59
|
+
@zip.close unless @skip_close
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def find(path, symlinks = Set.new)
|
65
|
+
name = PathUtils.join(@root, path)
|
66
|
+
entry = @zip.find_entry name
|
67
|
+
if entry.nil?
|
68
|
+
find_in_parent(path, symlinks)
|
69
|
+
elsif entry.symlink?
|
70
|
+
find_symlink(entry.get_input_stream(&:read), path, symlinks)
|
71
|
+
else
|
72
|
+
entry
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def find!(path)
|
77
|
+
find(path) || raise(Errno::ENOENT, path)
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_in_parent(path, symlinks)
|
81
|
+
directory, filename = File.split(path)
|
82
|
+
return if ['.', '/'].include? filename
|
83
|
+
entry = find(directory, symlinks)
|
84
|
+
return unless entry && entry.directory?
|
85
|
+
@zip.find_entry File.join(entry.name, filename)
|
86
|
+
end
|
87
|
+
|
88
|
+
def find_symlink(path, symlink, symlinks)
|
89
|
+
return if path.start_with?('/') || symlinks.include?(symlink)
|
90
|
+
symlinks << symlink
|
91
|
+
resolved_path = PathUtils.join(File.dirname(symlink), path)
|
92
|
+
find(resolved_path, symlinks)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'cfbundle/storage/base'
|
2
|
+
require 'cfbundle/storage/file_system'
|
3
|
+
require 'cfbundle/storage/zip'
|
4
|
+
|
5
|
+
module CFBundle
|
6
|
+
# @private
|
7
|
+
#
|
8
|
+
# Utility methods required to detect and instantiate a bundle's storage.
|
9
|
+
module StorageDetection
|
10
|
+
class << self
|
11
|
+
# Opens a file and returns a bundle storage.
|
12
|
+
# @param file The file to open.
|
13
|
+
# @return [Storage]
|
14
|
+
def open(file)
|
15
|
+
storage = open_as_storage(file) ||
|
16
|
+
open_as_io(file) ||
|
17
|
+
open_as_path(file)
|
18
|
+
raise "#{file.inspect} is not a bundle" unless storage
|
19
|
+
storage
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def path_for(file)
|
25
|
+
if file.is_a? String
|
26
|
+
file
|
27
|
+
elsif file.respond_to? :to_path
|
28
|
+
file.to_path
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def open_as_storage(file)
|
33
|
+
file if file.is_a?(Storage::Base)
|
34
|
+
end
|
35
|
+
|
36
|
+
def open_as_path(file)
|
37
|
+
path = path_for(file) || return
|
38
|
+
ext = File.extname(path)
|
39
|
+
if File.directory?(path) && ext != ''
|
40
|
+
Storage::FileSystem.new(path)
|
41
|
+
elsif ['.ipa', '.zip'].include? ext
|
42
|
+
open_zip_path(path)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def open_as_io(file)
|
47
|
+
open_zip_io(file.to_io) if file.respond_to?(:to_io)
|
48
|
+
end
|
49
|
+
|
50
|
+
def open_zip_path(path)
|
51
|
+
open_zip_file(path, &:open)
|
52
|
+
end
|
53
|
+
|
54
|
+
def open_zip_io(io)
|
55
|
+
open_zip_file(io, &:open_buffer)
|
56
|
+
end
|
57
|
+
|
58
|
+
def open_zip_file(file, skip_close: false)
|
59
|
+
unless defined?(Zip)
|
60
|
+
raise "cannot open ZIP archive #{file.inspect} without Rubyzip"
|
61
|
+
end
|
62
|
+
zip = yield(Zip::File, file)
|
63
|
+
entry = matching_zip_entry(zip)
|
64
|
+
Storage::Zip.new(zip, entry.name, skip_close: skip_close)
|
65
|
+
end
|
66
|
+
|
67
|
+
def matching_zip_entry(zip)
|
68
|
+
entries = zip.entries.select { |entry| zip_entry_match?(entry) }
|
69
|
+
case entries.count
|
70
|
+
when 1
|
71
|
+
entries.first
|
72
|
+
when 0
|
73
|
+
raise "no bundle found in ZIP archive \"#{zip}\""
|
74
|
+
else
|
75
|
+
raise "several bundles found in ZIP archive \"#{zip}\""
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def zip_entry_match?(entry)
|
80
|
+
entry.directory? &&
|
81
|
+
[nil, 'Payload/'].include?(entry.parent_as_string) &&
|
82
|
+
File.extname(entry.name) != ''
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/cfbundle.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'cfbundle/bundle'
|
2
|
+
require 'cfbundle/constants'
|
3
|
+
require 'cfbundle/storage/file_system'
|
4
|
+
require 'cfbundle/storage/zip'
|
5
|
+
require 'cfbundle/version'
|
6
|
+
|
7
|
+
# Module for reading macOS and iOS bundles.
|
8
|
+
#
|
9
|
+
# See {CFBundle::Bundle.open} for opening bundles.
|
10
|
+
module CFBundle
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cfbundle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicolas Bachschmidt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: CFPropertyList
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.3.5
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.3.5
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.0.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rspec
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 3.7.0
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.7.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rubocop
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.51.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.51.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rubyzip
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 1.2.1
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.2.1
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: simplecov
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 0.15.1
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.15.1
|
89
|
+
description: CFBundle is a module for reading macOS and iOS bundles (including zipped
|
90
|
+
bundles and .ipa files).
|
91
|
+
email: nicolas@h2g.io
|
92
|
+
executables: []
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- lib/cfbundle.rb
|
97
|
+
- lib/cfbundle/bundle.rb
|
98
|
+
- lib/cfbundle/constants.rb
|
99
|
+
- lib/cfbundle/localization.rb
|
100
|
+
- lib/cfbundle/path_utils.rb
|
101
|
+
- lib/cfbundle/plist.rb
|
102
|
+
- lib/cfbundle/resource.rb
|
103
|
+
- lib/cfbundle/storage/base.rb
|
104
|
+
- lib/cfbundle/storage/file_system.rb
|
105
|
+
- lib/cfbundle/storage/zip.rb
|
106
|
+
- lib/cfbundle/storage_detection.rb
|
107
|
+
- lib/cfbundle/version.rb
|
108
|
+
homepage:
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 2.6.11
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: CFBundle
|
132
|
+
test_files: []
|