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 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
@@ -0,0 +1,3 @@
1
+ module CFBundle
2
+ VERSION = '0.1.0'.freeze
3
+ 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: []