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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +19 -0
- data/Rakefile +8 -23
- data/hoodie.gemspec +0 -23
- data/lib/hoodie.rb +40 -48
- data/lib/hoodie/configuration.rb +39 -2
- data/lib/hoodie/core_ext/blank.rb +6 -6
- data/lib/hoodie/core_ext/hash.rb +108 -13
- data/lib/hoodie/core_ext/string.rb +5 -3
- data/lib/hoodie/core_ext/try.rb +4 -3
- data/lib/hoodie/inflections.rb +18 -0
- data/lib/hoodie/inflections/defaults.rb +17 -0
- data/lib/hoodie/inflections/inflections.rb +17 -0
- data/lib/hoodie/inflections/rules_collection.rb +17 -0
- data/lib/hoodie/logging.rb +22 -4
- data/lib/hoodie/stash.rb +83 -80
- data/lib/hoodie/stash/disk_store.rb +142 -118
- data/lib/hoodie/stash/mem_store.rb +10 -9
- data/lib/hoodie/stash/memoizable.rb +46 -0
- data/lib/hoodie/utils.rb +13 -183
- data/lib/hoodie/utils/ansi.rb +199 -0
- data/lib/hoodie/utils/crypto.rb +288 -0
- data/lib/hoodie/utils/equalizer.rb +146 -0
- data/lib/hoodie/utils/file_helper.rb +225 -0
- data/lib/hoodie/utils/konstruktor.rb +77 -0
- data/lib/hoodie/utils/machine.rb +83 -0
- data/lib/hoodie/utils/os.rb +56 -0
- data/lib/hoodie/utils/retry.rb +235 -0
- data/lib/hoodie/utils/timeout.rb +54 -0
- data/lib/hoodie/utils/url_helper.rb +104 -0
- data/lib/hoodie/version.rb +4 -4
- metadata +13 -234
- data/lib/hoodie/identity_map.rb +0 -96
- data/lib/hoodie/memoizable.rb +0 -43
- data/lib/hoodie/obfuscate.rb +0 -121
- data/lib/hoodie/observable.rb +0 -309
- data/lib/hoodie/os.rb +0 -43
- data/lib/hoodie/proxy.rb +0 -68
- data/lib/hoodie/rash.rb +0 -125
- 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
|