hoodie 0.5.5 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Gemfile +19 -0
  4. data/Rakefile +8 -23
  5. data/hoodie.gemspec +0 -23
  6. data/lib/hoodie.rb +40 -48
  7. data/lib/hoodie/configuration.rb +39 -2
  8. data/lib/hoodie/core_ext/blank.rb +6 -6
  9. data/lib/hoodie/core_ext/hash.rb +108 -13
  10. data/lib/hoodie/core_ext/string.rb +5 -3
  11. data/lib/hoodie/core_ext/try.rb +4 -3
  12. data/lib/hoodie/inflections.rb +18 -0
  13. data/lib/hoodie/inflections/defaults.rb +17 -0
  14. data/lib/hoodie/inflections/inflections.rb +17 -0
  15. data/lib/hoodie/inflections/rules_collection.rb +17 -0
  16. data/lib/hoodie/logging.rb +22 -4
  17. data/lib/hoodie/stash.rb +83 -80
  18. data/lib/hoodie/stash/disk_store.rb +142 -118
  19. data/lib/hoodie/stash/mem_store.rb +10 -9
  20. data/lib/hoodie/stash/memoizable.rb +46 -0
  21. data/lib/hoodie/utils.rb +13 -183
  22. data/lib/hoodie/utils/ansi.rb +199 -0
  23. data/lib/hoodie/utils/crypto.rb +288 -0
  24. data/lib/hoodie/utils/equalizer.rb +146 -0
  25. data/lib/hoodie/utils/file_helper.rb +225 -0
  26. data/lib/hoodie/utils/konstruktor.rb +77 -0
  27. data/lib/hoodie/utils/machine.rb +83 -0
  28. data/lib/hoodie/utils/os.rb +56 -0
  29. data/lib/hoodie/utils/retry.rb +235 -0
  30. data/lib/hoodie/utils/timeout.rb +54 -0
  31. data/lib/hoodie/utils/url_helper.rb +104 -0
  32. data/lib/hoodie/version.rb +4 -4
  33. metadata +13 -234
  34. data/lib/hoodie/identity_map.rb +0 -96
  35. data/lib/hoodie/memoizable.rb +0 -43
  36. data/lib/hoodie/obfuscate.rb +0 -121
  37. data/lib/hoodie/observable.rb +0 -309
  38. data/lib/hoodie/os.rb +0 -43
  39. data/lib/hoodie/proxy.rb +0 -68
  40. data/lib/hoodie/rash.rb +0 -125
  41. data/lib/hoodie/timers.rb +0 -355
@@ -0,0 +1,288 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'openssl'
21
+ require 'digest/sha2'
22
+ require 'base64'
23
+ require 'securerandom'
24
+
25
+ module Hoodie
26
+ # Crypto uses the AES-256-CBC algorithm by default to encrypt strings
27
+ # securely. It uses both an initialization vector (IV) and a salt to perform
28
+ # this encryption as securely as possible.
29
+ #
30
+ # @example
31
+ # Use `#encrypt` to encrypt a string.
32
+ # text = "what is 42?"
33
+ # salt = "9e5f851900cad8892ac8b737b7370cbe"
34
+ # pass = "!mWh0!s@y!m"
35
+ # encrypted_text = Crypto.encrypt(text, set_password, set_salt)
36
+ # # => "+opVpqJhQsD3dbOQ8GAGjmq7slIms2zCQmOrMxJGpqQ=\n"
37
+ #
38
+ # Then to decrypt the string use `#decrypt`.
39
+ # Crypto.decrypt(encrypted_text, pass, salt)
40
+ # # => "what is 42?"
41
+ #
42
+ # You can also set the salt and password on a configuration object.
43
+ # Hoodie::Crypto.config do |config|
44
+ # config.password = "!mWh0!s@y!m"
45
+ # config.salt = "9e5f851900cad8892ac8b737b7370cbe"
46
+ # end
47
+ #
48
+ # Now you can #encrypt and #decrypt without specifying a salt and password.
49
+ # encrypted_text = Crypto.encrypt(text)
50
+ # # => "HQRabUG8BcS+yZR8yG9TqQWfFPFYXztRgoQjdAUseFU=\n"
51
+ # Crypto.decrypt(encrypted_text)
52
+ # # => "what is 42?"
53
+ #
54
+ # What you probably want to use this for is directly on a String.
55
+ # encrypted_text = text.encrypt
56
+ # # => "ew2SEyf+09WdPJHRjmBGp4g6C1oSQaDbQiZ/7WEceEc=\n"
57
+ # encrypted_text.decrypt
58
+ # # => "what is 42?"
59
+ #
60
+ # @note
61
+ # The salt needs to be unique per-use per-encrypted string. Every time a
62
+ # string is encrypted, it should be hashed using a new random salt. Never
63
+ # reuse a salt. The salt also needs to be long, so that there are many
64
+ # possible salts. As a rule of thumb, the salt should be at least 32 random
65
+ # bytes. Hoodie includes a easy helper for you to generate a random binary
66
+ # string, `String.random_binary(SIZE)`, where size is the size in bytes.
67
+ #
68
+ module Crypto
69
+ extend self
70
+
71
+ # Adds `encrypt` and `decrypt` methods to strings.
72
+ #
73
+ module String
74
+ # Returns a new string containing the encrypted version of itself
75
+ #
76
+ def encrypt(password = nil, salt = nil)
77
+ Hoodie::Crypto.encrypt(self, password, salt)
78
+ end
79
+
80
+ # Returns a new string containing the decrypted version of itself
81
+ #
82
+ def decrypt(password = nil, salt = nil)
83
+ Hoodie::Crypto.decrypt(self, password, salt)
84
+ end
85
+
86
+ # Generate a random binary string of +n_bytes+ size.
87
+ #
88
+ def random_binary(n_bytes)
89
+ #(Array.new(n_bytes) { rand(0x100) }).pack('c*')
90
+ SecureRandom.random_bytes(64)
91
+ end
92
+ end
93
+
94
+ # A Configuration instance
95
+ class Configuration
96
+
97
+ # @!attribute [rw] :password
98
+ # @return [String] access the password for this instance.
99
+ attr_accessor :password
100
+
101
+ # @!attribute [rw] :salt
102
+ # @return [String] access the salt for this instance.
103
+ attr_accessor :salt
104
+
105
+ # Initialized a configuration instance
106
+ #
107
+ # @return [undefined]
108
+ #
109
+ # @api private
110
+ def initialize(options = {})
111
+ @password = options.fetch(:password, nil)
112
+ @salt = options.fetch(:salt, nil)
113
+
114
+ yield self if block_given?
115
+ end
116
+
117
+ # @api private
118
+ def to_h
119
+ { password: password, salt: salt }.freeze
120
+ end
121
+ end
122
+
123
+ # The default size, iterations and cipher encryption algorithm used.
124
+ SALT_BYTE_SIZE = 64
125
+ HASH_BYTE_SIZE = 256
126
+ CRYPTERATIONS = 4096
127
+ CIPHER_TYPE = 'aes-256-cbc'
128
+
129
+ # Encrypt the given string using the AES-256-CBC algorithm.
130
+ #
131
+ # @param [String] plain_text
132
+ # The text to encrypt.
133
+ #
134
+ # @param [String] password
135
+ # Secret passphrase to encrypt with.
136
+ #
137
+ # @param [String] salt
138
+ # A cryptographically secure pseudo-random string (SecureRandom.base64)
139
+ # to add a little spice to your encryption.
140
+ #
141
+ # @return [String]
142
+ # Encrypted text, can be deciphered with #decrypt.
143
+ #
144
+ # @api public
145
+ def encrypt(plain_text, password = nil, salt = nil)
146
+ password = password.nil? ? Hoodie.crypto.password : password
147
+ salt = salt.nil? ? Hoodie.crypto.salt : salt
148
+
149
+ cipher = new_cipher(:encrypt, password, salt)
150
+ cipher.iv = iv = cipher.random_iv
151
+ ciphertext = cipher.update(plain_text)
152
+ ciphertext << cipher.final
153
+ Base64.encode64(combine_iv_ciphertext(iv, ciphertext))
154
+ end
155
+
156
+ # Decrypt the given string, using the salt and password supplied.
157
+ #
158
+ # @param [String] encrypted_text
159
+ # The text to decrypt, probably produced with #decrypt.
160
+ #
161
+ # @param [String] password
162
+ # Secret passphrase to decrypt with.
163
+ #
164
+ # @param [String] salt
165
+ # The cryptographically secure pseudo-random string used to spice up the
166
+ # encryption of your strings.
167
+ #
168
+ # @return [String]
169
+ # The decrypted plain_text.
170
+ #
171
+ # @api public
172
+ def decrypt(encrypted_text, password = nil, salt = nil)
173
+ password = password.nil? ? Hoodie.crypto.password : password
174
+ salt = salt.nil? ? Hoodie.crypto.salt : salt
175
+
176
+ iv_ciphertext = Base64.decode64(encrypted_text)
177
+ cipher = new_cipher(:decrypt, password, salt)
178
+ cipher.iv, ciphertext = separate_iv_ciphertext(cipher, iv_ciphertext)
179
+ plain_text = cipher.update(ciphertext)
180
+ plain_text << cipher.final
181
+ plain_text
182
+ end
183
+
184
+ # Generates a special hash known as a SPASH, a PBKDF2-HMAC-SHA1 Salted
185
+ # Password Hash for safekeeping.
186
+ #
187
+ # @param [String] password
188
+ # A password to generating the SPASH, salted password hash.
189
+ #
190
+ # @return [Hash]
191
+ # `:salt` contains the unique salt used, `:pbkdf2` contains the password
192
+ # hash. Save both the salt and the hash together.
193
+ #
194
+ # @see Hoodie::Crypto#validate_salt
195
+ #
196
+ # @api public
197
+ def salted_hash(password)
198
+ salt = SecureRandom.random_bytes(SALT_BYTE_SIZE)
199
+ pbkdf2 = OpenSSL::PKCS5::pbkdf2_hmac_sha1(
200
+ password,
201
+ salt,
202
+ CRYPTERATIONS,
203
+ HASH_BYTE_SIZE)
204
+
205
+ { salt: salt, pbkdf2: Base64.encode64(pbkdf2) }
206
+ end
207
+
208
+ private # P R O P R I E T À P R I V A T A Vietato L'accesso
209
+
210
+ # Validates a salted PBKDF2-HMAC-SHA1 hash of a password.
211
+ #
212
+ # @param [String] password
213
+ # The password used to create the SPASH, salted password hash.
214
+ #
215
+ # @param opts [Hash]
216
+ #
217
+ # @option opts [String] :salt
218
+ # The salt used in generating the SPASH, salted password hash.
219
+ #
220
+ # @option opts [String] :hash
221
+ # The hash produced when salt and password collided in a algorithm of
222
+ # PBKDF2-HMAC-SHA1 love bites (do you tell lies?) hash.
223
+ #
224
+ # @return [Boolean]
225
+ # True if the password is a match, false if ménage à trois of salt, hash
226
+ # and password don't mix.
227
+ #
228
+ # @see Hoodie::Crypto#salted_hash
229
+ #
230
+ # @api private
231
+ def validate_salt(password, hash = {})
232
+ pbkdf2 = Base64.decode64(hash[:pbkdf2])
233
+ salty = OpenSSL::PKCS5::pbkdf2_hmac_sha1(
234
+ password,
235
+ hash[:salt],
236
+ CRYPTERATIONS,
237
+ HASH_BYTE_SIZE)
238
+ pbkdf2 == salty
239
+ end
240
+
241
+ protected # A T T E N Z I O N E A R E A P R O T E T T A
242
+
243
+ # Create a new cipher machine, with its dials set in the given direction.
244
+ #
245
+ # @param [Symbol] direction
246
+ # Whether to `:encrypt` or `:decrypt`.
247
+ #
248
+ # @param [String] pass
249
+ # Secret passphrase to decrypt with.
250
+ #
251
+ # @api private
252
+ def new_cipher(direction, password, salt)
253
+ cipher = OpenSSL::Cipher::Cipher.new(CIPHER_TYPE)
254
+ direction == :encrypt ? cipher.encrypt : cipher.decrypt
255
+ cipher.key = encrypt_key(password, salt)
256
+ cipher
257
+ end
258
+
259
+ # Prepend the initialization vector to the encoded message.
260
+ #
261
+ # @api private
262
+ def combine_iv_ciphertext(iv, message)
263
+ message.force_encoding('BINARY') if message.respond_to?(:force_encoding)
264
+ iv.force_encoding('BINARY') if iv.respond_to?(:force_encoding)
265
+ iv + message
266
+ end
267
+
268
+ # Pull the initialization vector from the front of the encoded message.
269
+ #
270
+ # @api private
271
+ def separate_iv_ciphertext(cipher, iv_ciphertext)
272
+ idx = cipher.iv_len
273
+ [iv_ciphertext[0..(idx - 1)], iv_ciphertext[idx..-1]]
274
+ end
275
+
276
+ # Convert the password into a PBKDF2-HMAC-SHA1 salted key used for safely
277
+ # encrypting and decrypting all your ciphers strings.
278
+ #
279
+ # @api private
280
+ def encrypt_key(password, salt)
281
+ iterations, length = CRYPTERATIONS, HASH_BYTE_SIZE
282
+ OpenSSL::PKCS5::pbkdf2_hmac_sha1(password, salt, iterations, length)
283
+ end
284
+ end
285
+ end
286
+
287
+ # Adds `encrypt` and `decrypt` methods to strings.
288
+ String.send(:include, Hoodie::Crypto::String)
@@ -0,0 +1,146 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ module Hoodie
21
+ # Define equality, equivalence and inspection methods.
22
+ #
23
+ class Equalizer
24
+
25
+ # Initialize an Equalizer with the given keys.
26
+ #
27
+ # Will use the keys with which it is initialized to define #cmp?,
28
+ # #hash, and #inspect
29
+ #
30
+ # @param [String] name
31
+ #
32
+ # @param [Array<Symbol>] keys
33
+ #
34
+ # @return [undefined]
35
+ #
36
+ # @api private
37
+ def initialize(name, keys = [])
38
+ @name = name.dup.freeze
39
+ @keys = keys.dup
40
+ define_methods
41
+ include_comparison_methods
42
+ end
43
+
44
+ # Append a key and compile the equality methods.
45
+ #
46
+ # @return [Equalizer] self
47
+ #
48
+ # @api private
49
+ def <<(key)
50
+ @keys << key
51
+ self
52
+ end
53
+
54
+ private # P R O P R I E T À P R I V A T A Vietato L'accesso
55
+
56
+ # Define the equalizer methods based on #keys.
57
+ #
58
+ # @return [undefined]
59
+ #
60
+ # @api private
61
+ def define_methods
62
+ define_cmp_method
63
+ define_hash_method
64
+ define_inspect_method
65
+ end
66
+
67
+ # Define an #cmp? method based on the instance's values identified by #keys.
68
+ #
69
+ # @return [undefined]
70
+ #
71
+ # @api private
72
+ def define_cmp_method
73
+ keys = @keys
74
+ define_method(:cmp?) do |comparator, other|
75
+ keys.all? { |key| send(key).send(comparator, other.send(key)) }
76
+ end
77
+ end
78
+
79
+ # Define a #hash method based on the instance's values identified by #keys.
80
+ #
81
+ # @return [undefined]
82
+ #
83
+ # @api private
84
+ def define_hash_method
85
+ keys = @keys
86
+ define_method(:hash) do
87
+ keys.map { |key| send(key) }.push(self.class).hash
88
+ end
89
+ end
90
+
91
+ # Define an inspect method that reports the values of the instance's keys.
92
+ #
93
+ # @return [undefined]
94
+ #
95
+ # @api private
96
+ def define_inspect_method
97
+ name, keys = @name, @keys
98
+ define_method(:inspect) do
99
+ "#<#{name}#{keys.map { |key| " #{key}=#{send(key).inspect}" }.join}>"
100
+ end
101
+ end
102
+
103
+ # Include the #eql? and #== methods
104
+ #
105
+ # @return [undefined]
106
+ #
107
+ # @api private
108
+ def include_comparison_methods
109
+ module_eval { include Methods }
110
+ end
111
+
112
+ # The comparison methods
113
+ module Methods
114
+
115
+ # Compare the object with other object for equality.
116
+ #
117
+ # @example
118
+ # object.eql?(other) # => true or false
119
+ #
120
+ # @param [Object] other
121
+ # the other object to compare with
122
+ #
123
+ # @return [Boolean]
124
+ #
125
+ # @api public
126
+ def eql?(other)
127
+ instance_of?(other.class) && cmp?(__method__, other)
128
+ end
129
+
130
+ # Compare the object with other object for equivalency.
131
+ #
132
+ # @example
133
+ # object == other # => true or false
134
+ #
135
+ # @param [Object] other
136
+ # the other object to compare with
137
+ #
138
+ # @return [Boolean]
139
+ #
140
+ # @api public
141
+ def ==(other)
142
+ other.kind_of?(self.class) && cmp?(__method__, other)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,225 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ module Hoodie
21
+ # Class methods that are added when you include Hoodie
22
+ #
23
+ module FileHelper
24
+ # Methods are also available as module-level methods as well as a mixin.
25
+ extend self
26
+
27
+ # Checks in PATH returns true if the command is found.
28
+ #
29
+ # @param [String] command
30
+ # The name of the command to look for.
31
+ #
32
+ # @return [Boolean]
33
+ # True if the command is found in the path.
34
+ #
35
+ def command_in_path?(command)
36
+ found = ENV['PATH'].split(File::PATH_SEPARATOR).map do |p|
37
+ File.exist?(File.join(p, command))
38
+ end
39
+ found.include?(true)
40
+ end
41
+
42
+ if const_defined?(:Win32Exts)
43
+ Win32Exts.concat %w{.exe .com .bat .cmd}
44
+ Win32Exts.uniq!
45
+ else
46
+ Win32Exts = %w{.exe .com .bat .cmd}
47
+ end
48
+
49
+ # Looks for the first occurrence of program within path. On the pure crap
50
+ # OS, also known as Windows, it looks for executables ending with .exe,
51
+ # .bat and .com, which you may optionally include in the program name.
52
+ #
53
+ # @param [String] cmd
54
+ # The name of the command to find.
55
+ #
56
+ # @param [String] path
57
+ # The path to search for the command.
58
+ #
59
+ # @return [String, NilClass]
60
+ #
61
+ # @api public
62
+ def which(prog, path = ENV['PATH'])
63
+ path.split(File::PATH_SEPARATOR).each do |dir|
64
+ if File::ALT_SEPARATOR
65
+ ext = Win32Exts.find do |ext|
66
+ if prog.include?('.')
67
+ f = File.join(dir, prog)
68
+ else
69
+ f = File.join(dir, prog+ext)
70
+ end
71
+ File.executable?(f) && !File.directory?(f)
72
+ end
73
+ if ext
74
+ if prog.include?('.')
75
+ f = File.join(dir, prog).gsub(/\//,'\\')
76
+ else
77
+ f = File.join(dir, prog + ext).gsub(/\//,'\\')
78
+ end
79
+ return f
80
+ end
81
+ else
82
+ f = File.join(dir, prog)
83
+ if File.executable?(f) && !File.directory?(f)
84
+ return File::join(dir, prog)
85
+ end
86
+ end
87
+ end
88
+
89
+ nil
90
+ end
91
+
92
+ # In block form, yields each program within path. In non-block form,
93
+ # returns an array of each program within path. Returns nil if not found
94
+ # found. On the Shit for Windows platform, it looks for executables
95
+ # ending with .exe, .bat and .com, which you may optionally include in
96
+ # the program name.
97
+ #
98
+ # @example
99
+ # whereis('ruby')
100
+ # # => [
101
+ # [0] "/opt/chefdk/embedded/bin/ruby",
102
+ # [1] "/usr/bin/ruby",
103
+ # [2] "/Users/sharding/.rvm/rubies/ruby-2.2.0/bin/ruby",
104
+ # [3] "/usr/bin/ruby"
105
+ # ]
106
+ #
107
+ # @param [String] cmd
108
+ # The name of the command to find.
109
+ #
110
+ # @param [String] path
111
+ # The path to search for the command.
112
+ #
113
+ # @return [String, Array, NilClass]
114
+ #
115
+ # @api public
116
+ def whereis(prog, path=ENV['PATH'])
117
+ dirs = []
118
+ path.split(File::PATH_SEPARATOR).each do |dir|
119
+ if File::ALT_SEPARATOR
120
+ if prog.include?('.')
121
+ f = File.join(dir,prog)
122
+ if File.executable?(f) && !File.directory?(f)
123
+ if block_given?
124
+ yield f.gsub(/\//,'\\')
125
+ else
126
+ dirs << f.gsub(/\//,'\\')
127
+ end
128
+ end
129
+ else
130
+ Win32Exts.find_all do |ext|
131
+ f = File.join(dir,prog+ext)
132
+ if File.executable?(f) && !File.directory?(f)
133
+ if block_given?
134
+ yield f.gsub(/\//,'\\')
135
+ else
136
+ dirs << f.gsub(/\//,'\\')
137
+ end
138
+ end
139
+ end
140
+ end
141
+ else
142
+ f = File.join(dir,prog)
143
+ if File.executable?(f) && !File.directory?(f)
144
+ if block_given?
145
+ yield f
146
+ else
147
+ dirs << f
148
+ end
149
+ end
150
+ end
151
+ end
152
+ dirs.empty? ? nil : dirs
153
+ end
154
+
155
+ # Get a recusive list of files inside a path.
156
+ #
157
+ # @param [String] path
158
+ # some path string or Pathname
159
+ # @param [Block] ignore
160
+ # a proc/block that returns true if a given path should be ignored, if a
161
+ # path is ignored, nothing below it will be searched either.
162
+ #
163
+ # @return [Array<Pathname>]
164
+ # array of Pathnames for each file (no directories)
165
+ #
166
+ def all_files_under(path, &ignore)
167
+ path = Pathname(path)
168
+
169
+ if path.directory?
170
+ path.children.flat_map do |child|
171
+ all_files_under(child, &ignore)
172
+ end.compact
173
+ elsif path.file?
174
+ if block_given? && ignore.call(path)
175
+ []
176
+ else
177
+ [path]
178
+ end
179
+ else
180
+ []
181
+ end
182
+ end
183
+
184
+ # Takes an object, which can be a literal string or a string containing
185
+ # glob expressions, or a regexp, or a proc, or anything else that responds
186
+ # to #match or #call, and returns whether or not the given path matches
187
+ # that matcher.
188
+ #
189
+ # @param [String, #match, #call] matcher
190
+ # a matcher String, RegExp, Proc, etc.
191
+ #
192
+ # @param [String] path
193
+ # a path as a string
194
+ #
195
+ # @return [Boolean]
196
+ # whether the path matches the matcher
197
+ #
198
+ def path_match(matcher, path)
199
+ case
200
+ when matcher.is_a?(String)
201
+ if matcher.include? '*'
202
+ File.fnmatch(matcher, path)
203
+ else
204
+ path == matcher
205
+ end
206
+ when matcher.respond_to?(:match)
207
+ !matcher.match(path).nil?
208
+ when matcher.respond_to?(:call)
209
+ matcher.call(path)
210
+ else
211
+ File.fnmatch(matcher.to_s, path)
212
+ end
213
+ end
214
+
215
+ # Normalize a path to not include a leading slash
216
+ #
217
+ # @param [String] path
218
+ #
219
+ # @return [String]
220
+ #
221
+ def normalize_path(path)
222
+ path.sub(%r{^/}, '').tr('', '')
223
+ end
224
+ end
225
+ end