chef-config 17.9.52 → 17.10.19

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.
@@ -1,350 +1,350 @@
1
- #
2
- # Author:: Bryan McLellan <btm@loftninjas.org>
3
- # Copyright:: Copyright (c) Chef Software Inc.
4
- # License:: Apache License, Version 2.0
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #
18
-
19
- require "chef-utils" unless defined?(ChefUtils::CANARY)
20
- require_relative "windows"
21
- require_relative "logger"
22
- require_relative "exceptions"
23
-
24
- module ChefConfig
25
- class PathHelper
26
- # Maximum characters in a standard Windows path (260 including drive letter and NUL)
27
- WIN_MAX_PATH = 259
28
-
29
- def self.dirname(path, windows: ChefUtils.windows?)
30
- if windows
31
- # Find the first slash, not counting trailing slashes
32
- end_slash = path.size
33
- loop do
34
- slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator(windows: windows))}]/, end_slash - 1)
35
- if !slash
36
- return end_slash == path.size ? "." : path_separator(windows: windows)
37
- elsif slash == end_slash - 1
38
- end_slash = slash
39
- else
40
- return path[0..slash - 1]
41
- end
42
- end
43
- else
44
- ::File.dirname(path)
45
- end
46
- end
47
-
48
- BACKSLASH = "\\".freeze
49
-
50
- def self.path_separator(windows: ChefUtils.windows?)
51
- if windows
52
- BACKSLASH
53
- else
54
- File::SEPARATOR
55
- end
56
- end
57
-
58
- def self.join(*args, windows: ChefUtils.windows?)
59
- path_separator_regex = Regexp.escape(windows ? "#{File::SEPARATOR}#{BACKSLASH}" : File::SEPARATOR)
60
- trailing_slashes_regex = /[#{path_separator_regex}]+$/.freeze
61
- leading_slashes_regex = /^[#{path_separator_regex}]+/.freeze
62
-
63
- args.flatten.inject do |joined_path, component|
64
- joined_path = joined_path.sub(trailing_slashes_regex, "")
65
- component = component.sub(leading_slashes_regex, "")
66
- joined_path + "#{path_separator(windows: windows)}#{component}"
67
- end
68
- end
69
-
70
- def self.validate_path(path, windows: ChefUtils.windows?)
71
- if windows
72
- unless printable?(path)
73
- msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings."
74
- ChefConfig.logger.error(msg)
75
- raise ChefConfig::InvalidPath, msg
76
- end
77
-
78
- if windows_max_length_exceeded?(path)
79
- ChefConfig.logger.trace("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'")
80
- path.insert(0, "\\\\?\\")
81
- end
82
- end
83
-
84
- path
85
- end
86
-
87
- def self.windows_max_length_exceeded?(path)
88
- # Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API
89
- # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
90
- unless /^\\\\?\\/.match?(path)
91
- if path.length > WIN_MAX_PATH
92
- return true
93
- end
94
- end
95
-
96
- false
97
- end
98
-
99
- def self.printable?(string)
100
- # returns true if string is free of non-printable characters (escape sequences)
101
- # this returns false for whitespace escape sequences as well, e.g. \n\t
102
- if /[^[:print:]]/.match?(string)
103
- false
104
- else
105
- true
106
- end
107
- end
108
-
109
- # Produces a comparable path.
110
- def self.canonical_path(path, add_prefix = true, windows: ChefUtils.windows?)
111
- # First remove extra separators and resolve any relative paths
112
- abs_path = File.absolute_path(path)
113
-
114
- if windows
115
- # Add the \\?\ API prefix on Windows unless add_prefix is false
116
- # Downcase on Windows where paths are still case-insensitive
117
- abs_path.gsub!(::File::SEPARATOR, path_separator(windows: windows))
118
- if add_prefix && abs_path !~ /^\\\\?\\/
119
- abs_path.insert(0, "\\\\?\\")
120
- end
121
-
122
- abs_path.downcase!
123
- end
124
-
125
- abs_path
126
- end
127
-
128
- # The built in ruby Pathname#cleanpath method does not clean up forward slashes and
129
- # backslashes. This is a wrapper around that which does. In general this is NOT
130
- # recommended for internal use within ruby/chef since ruby does not care about forward slashes
131
- # vs. backslashes, even on Windows. Where this generally matters is when being rendered
132
- # to the user, or being rendered into things like the windows PATH or to commands that
133
- # are being executed. In some cases it may be easier on windows to render paths to
134
- # unix-style for being eventually eval'd by ruby in the future (templates being rendered
135
- # with code to be consumed by ruby) where forcing unix-style forward slashes avoids the
136
- # issue of needing to escape the backslashes in rendered strings. This has a boolean
137
- # operator to force windows-style or non-windows style operation, where the default is
138
- # determined by the underlying node['platform'] value.
139
- #
140
- # In general if you don't know if you need this routine, do not use it, best practice
141
- # within chef/ruby itself is not to care. Only use it to force windows or unix style
142
- # when it really matters.
143
- #
144
- # @param path [String] the path to clean
145
- # @param windows [Boolean] optional flag to force to windows or unix-style
146
- # @return [String] cleaned path
147
- #
148
- def self.cleanpath(path, windows: ChefUtils.windows?)
149
- path = Pathname.new(path).cleanpath.to_s
150
- if windows
151
- # ensure all forward slashes are backslashes
152
- path.gsub(File::SEPARATOR, path_separator(windows: windows))
153
- else
154
- # ensure all backslashes are forward slashes
155
- path.gsub(BACKSLASH, File::SEPARATOR)
156
- end
157
- end
158
-
159
- # This is not just escaping for something like use in Regexps, or in globs. For the former
160
- # just use Regexp.escape. For the latter, use escape_glob_dir below.
161
- #
162
- # This is escaping where the path to be rendered is being put into a ruby file which will
163
- # later be read back by ruby (or something similar) so we need quadruple backslashes.
164
- #
165
- # In order to print:
166
- #
167
- # file_cache_path "C:\\chef"
168
- #
169
- # We need to convert "C:\chef" to "C:\\\\chef" to interpolate into a string which is rendered
170
- # into the output file with that line in it.
171
- #
172
- # @param path [String] the path to escape
173
- # @return [String] the escaped path
174
- #
175
- def self.escapepath(path)
176
- path.gsub(BACKSLASH, BACKSLASH * 4)
177
- end
178
-
179
- def self.paths_eql?(path1, path2, windows: ChefUtils.windows?)
180
- canonical_path(path1, windows: windows) == canonical_path(path2, windows: windows)
181
- end
182
-
183
- # @deprecated this method is deprecated. Please use escape_glob_dirs
184
- # Paths which may contain glob-reserved characters need
185
- # to be escaped before globbing can be done.
186
- # http://stackoverflow.com/questions/14127343
187
- def self.escape_glob(*parts, windows: ChefUtils.windows?)
188
- path = cleanpath(join(*parts, windows: windows), windows: windows)
189
- path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\" + x }
190
- end
191
-
192
- # This function does not switch to backslashes for windows
193
- # This is because only forwardslashes should be used with dir (even for windows)
194
- def self.escape_glob_dir(*parts)
195
- path = Pathname.new(join(*parts)).cleanpath.to_s
196
- path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\" + x }
197
- end
198
-
199
- def self.relative_path_from(from, to, windows: ChefUtils.windows?)
200
- Pathname.new(cleanpath(to, windows: windows)).relative_path_from(Pathname.new(cleanpath(from, windows: windows)))
201
- end
202
-
203
- # Set the project-specific home directory environment variable.
204
- #
205
- # This can be used to allow per-tool home directory aliases like $KNIFE_HOME.
206
- #
207
- # @param [env_var] Key for an environment variable to use.
208
- # @return [nil]
209
- def self.per_tool_home_environment=(env_var)
210
- @@per_tool_home_environment = env_var
211
- # Reset this in case .home was already called.
212
- @@home_dir = nil
213
- end
214
-
215
- # Retrieves the "home directory" of the current user while trying to ascertain the existence
216
- # of said directory. The path returned uses / for all separators (the ruby standard format).
217
- # If the home directory doesn't exist or an error is otherwise encountered, nil is returned.
218
- #
219
- # If a set of path elements is provided, they are appended as-is to the home path if the
220
- # homepath exists.
221
- #
222
- # If an optional block is provided, the joined path is passed to that block if the home path is
223
- # valid and the result of the block is returned instead.
224
- #
225
- # Home-path discovery is performed once. If a path is discovered, that value is memoized so
226
- # that subsequent calls to home_dir don't bounce around.
227
- #
228
- # @see all_homes
229
- # @param args [Array<String>] Path components to look for under the home directory.
230
- # @return [String]
231
- def self.home(*args)
232
- @@home_dir ||= all_homes { |p| break p }
233
- if @@home_dir
234
- path = File.join(@@home_dir, *args)
235
- block_given? ? (yield path) : path
236
- end
237
- end
238
-
239
- # See self.home. This method performs a similar operation except that it yields all the different
240
- # possible values of 'HOME' that one could have on this platform. Hence, on windows, if
241
- # HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice.
242
- # This method goes out and checks the existence of each location at the time of the call.
243
- #
244
- # The return is a list of all the returned values from each block invocation or a list of paths
245
- # if no block is provided.
246
- def self.all_homes(*args, windows: ChefUtils.windows?)
247
- paths = []
248
- paths << ENV[@@per_tool_home_environment] if defined?(@@per_tool_home_environment) && @@per_tool_home_environment && ENV[@@per_tool_home_environment]
249
- paths << ENV["CHEF_HOME"] if ENV["CHEF_HOME"]
250
- if windows
251
- # By default, Ruby uses the the following environment variables to determine Dir.home:
252
- # HOME
253
- # HOMEDRIVE HOMEPATH
254
- # USERPROFILE
255
- # Ruby only checks to see if the variable is specified - not if the directory actually exists.
256
- # On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive)
257
- # while USERPROFILE points to the location where the user application settings and profile are stored. HOME
258
- # is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is
259
- # HOMESHARE instead of HOMEDRIVE.
260
- #
261
- # We instead walk down the following and only include paths that actually exist.
262
- # HOME
263
- # HOMEDRIVE HOMEPATH
264
- # HOMESHARE HOMEPATH
265
- # USERPROFILE
266
-
267
- paths << ENV["HOME"]
268
- paths << ENV["HOMEDRIVE"] + ENV["HOMEPATH"] if ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
269
- paths << ENV["HOMESHARE"] + ENV["HOMEPATH"] if ENV["HOMESHARE"] && ENV["HOMEPATH"]
270
- paths << ENV["USERPROFILE"]
271
- end
272
- paths << Dir.home if ENV["HOME"]
273
-
274
- # Depending on what environment variables we're using, the slashes can go in any which way.
275
- # Just change them all to / to keep things consistent.
276
- # Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on
277
- # the particular brand of kool-aid you consume. This code assumes that \ and / are both
278
- # path separators on any system being used.
279
- paths = paths.map { |home_path| home_path.gsub(path_separator(windows: windows), ::File::SEPARATOR) if home_path }
280
-
281
- # Filter out duplicate paths and paths that don't exist.
282
- valid_paths = paths.select { |home_path| home_path && Dir.exist?(home_path.force_encoding("utf-8")) }
283
- valid_paths = valid_paths.uniq
284
-
285
- # Join all optional path elements at the end.
286
- # If a block is provided, invoke it - otherwise just return what we've got.
287
- joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) }
288
- if block_given?
289
- joined_paths.each { |p| yield p }
290
- else
291
- joined_paths
292
- end
293
- end
294
-
295
- # Determine if the given path is protected by macOS System Integrity Protection.
296
- def self.is_sip_path?(path, node)
297
- if ChefUtils.macos?
298
- # @todo: parse rootless.conf for this?
299
- sip_paths = [
300
- "/System", "/bin", "/sbin", "/usr"
301
- ]
302
- sip_paths.each do |sip_path|
303
- ChefConfig.logger.info("#{sip_path} is a SIP path, checking if it is in the exceptions list.")
304
- return true if path.start_with?(sip_path)
305
- end
306
- false
307
- else
308
- false
309
- end
310
- end
311
-
312
- # Determine if the given path is on the exception list for macOS System Integrity Protection.
313
- def self.writable_sip_path?(path)
314
- # todo: parse rootless.conf for this?
315
- sip_exceptions = [
316
- "/System/Library/Caches", "/System/Library/Extensions",
317
- "/System/Library/Speech", "/System/Library/User Template",
318
- "/usr/libexec/cups", "/usr/local", "/usr/share/man"
319
- ]
320
- sip_exceptions.each do |exception_path|
321
- return true if path.start_with?(exception_path)
322
- end
323
- ChefConfig.logger.error("Cannot write to a SIP path #{path} on macOS!")
324
- false
325
- end
326
-
327
- # Splits a string into an array of tokens as commands and arguments
328
- #
329
- # str = 'command with "some arguments"'
330
- # split_args(str) => ["command", "with", "\"some arguments\""]
331
- #
332
- def self.split_args(line)
333
- cmd_args = []
334
- field = ""
335
- line.scan(/\s*(?>([^\s\\"]+|"([^"]*)"|'([^']*)')|(\S))(\s|\z)?/m) do |word, within_dq, within_sq, esc, sep|
336
-
337
- # Append the string with Word & Escape Character
338
- field << (word || esc.gsub(/\\(.)/, "\\1"))
339
-
340
- # Re-build the field when any whitespace character or
341
- # End of string is encountered
342
- if sep
343
- cmd_args << field
344
- field = ""
345
- end
346
- end
347
- cmd_args
348
- end
349
- end
350
- end
1
+ #
2
+ # Author:: Bryan McLellan <btm@loftninjas.org>
3
+ # Copyright:: Copyright (c) Chef Software Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require "chef-utils" unless defined?(ChefUtils::CANARY)
20
+ require_relative "windows"
21
+ require_relative "logger"
22
+ require_relative "exceptions"
23
+
24
+ module ChefConfig
25
+ class PathHelper
26
+ # Maximum characters in a standard Windows path (260 including drive letter and NUL)
27
+ WIN_MAX_PATH = 259
28
+
29
+ def self.dirname(path, windows: ChefUtils.windows?)
30
+ if windows
31
+ # Find the first slash, not counting trailing slashes
32
+ end_slash = path.size
33
+ loop do
34
+ slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator(windows: windows))}]/, end_slash - 1)
35
+ if !slash
36
+ return end_slash == path.size ? "." : path_separator(windows: windows)
37
+ elsif slash == end_slash - 1
38
+ end_slash = slash
39
+ else
40
+ return path[0..slash - 1]
41
+ end
42
+ end
43
+ else
44
+ ::File.dirname(path)
45
+ end
46
+ end
47
+
48
+ BACKSLASH = "\\".freeze
49
+
50
+ def self.path_separator(windows: ChefUtils.windows?)
51
+ if windows
52
+ BACKSLASH
53
+ else
54
+ File::SEPARATOR
55
+ end
56
+ end
57
+
58
+ def self.join(*args, windows: ChefUtils.windows?)
59
+ path_separator_regex = Regexp.escape(windows ? "#{File::SEPARATOR}#{BACKSLASH}" : File::SEPARATOR)
60
+ trailing_slashes_regex = /[#{path_separator_regex}]+$/.freeze
61
+ leading_slashes_regex = /^[#{path_separator_regex}]+/.freeze
62
+
63
+ args.flatten.inject do |joined_path, component|
64
+ joined_path = joined_path.sub(trailing_slashes_regex, "")
65
+ component = component.sub(leading_slashes_regex, "")
66
+ joined_path + "#{path_separator(windows: windows)}#{component}"
67
+ end
68
+ end
69
+
70
+ def self.validate_path(path, windows: ChefUtils.windows?)
71
+ if windows
72
+ unless printable?(path)
73
+ msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings."
74
+ ChefConfig.logger.error(msg)
75
+ raise ChefConfig::InvalidPath, msg
76
+ end
77
+
78
+ if windows_max_length_exceeded?(path)
79
+ ChefConfig.logger.trace("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'")
80
+ path.insert(0, "\\\\?\\")
81
+ end
82
+ end
83
+
84
+ path
85
+ end
86
+
87
+ def self.windows_max_length_exceeded?(path)
88
+ # Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API
89
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
90
+ unless /^\\\\?\\/.match?(path)
91
+ if path.length > WIN_MAX_PATH
92
+ return true
93
+ end
94
+ end
95
+
96
+ false
97
+ end
98
+
99
+ def self.printable?(string)
100
+ # returns true if string is free of non-printable characters (escape sequences)
101
+ # this returns false for whitespace escape sequences as well, e.g. \n\t
102
+ if /[^[:print:]]/.match?(string)
103
+ false
104
+ else
105
+ true
106
+ end
107
+ end
108
+
109
+ # Produces a comparable path.
110
+ def self.canonical_path(path, add_prefix = true, windows: ChefUtils.windows?)
111
+ # First remove extra separators and resolve any relative paths
112
+ abs_path = File.absolute_path(path)
113
+
114
+ if windows
115
+ # Add the \\?\ API prefix on Windows unless add_prefix is false
116
+ # Downcase on Windows where paths are still case-insensitive
117
+ abs_path.gsub!(::File::SEPARATOR, path_separator(windows: windows))
118
+ if add_prefix && abs_path !~ /^\\\\?\\/
119
+ abs_path.insert(0, "\\\\?\\")
120
+ end
121
+
122
+ abs_path.downcase!
123
+ end
124
+
125
+ abs_path
126
+ end
127
+
128
+ # The built in ruby Pathname#cleanpath method does not clean up forward slashes and
129
+ # backslashes. This is a wrapper around that which does. In general this is NOT
130
+ # recommended for internal use within ruby/chef since ruby does not care about forward slashes
131
+ # vs. backslashes, even on Windows. Where this generally matters is when being rendered
132
+ # to the user, or being rendered into things like the windows PATH or to commands that
133
+ # are being executed. In some cases it may be easier on windows to render paths to
134
+ # unix-style for being eventually eval'd by ruby in the future (templates being rendered
135
+ # with code to be consumed by ruby) where forcing unix-style forward slashes avoids the
136
+ # issue of needing to escape the backslashes in rendered strings. This has a boolean
137
+ # operator to force windows-style or non-windows style operation, where the default is
138
+ # determined by the underlying node['platform'] value.
139
+ #
140
+ # In general if you don't know if you need this routine, do not use it, best practice
141
+ # within chef/ruby itself is not to care. Only use it to force windows or unix style
142
+ # when it really matters.
143
+ #
144
+ # @param path [String] the path to clean
145
+ # @param windows [Boolean] optional flag to force to windows or unix-style
146
+ # @return [String] cleaned path
147
+ #
148
+ def self.cleanpath(path, windows: ChefUtils.windows?)
149
+ path = Pathname.new(path).cleanpath.to_s
150
+ if windows
151
+ # ensure all forward slashes are backslashes
152
+ path.gsub(File::SEPARATOR, path_separator(windows: windows))
153
+ else
154
+ # ensure all backslashes are forward slashes
155
+ path.gsub(BACKSLASH, File::SEPARATOR)
156
+ end
157
+ end
158
+
159
+ # This is not just escaping for something like use in Regexps, or in globs. For the former
160
+ # just use Regexp.escape. For the latter, use escape_glob_dir below.
161
+ #
162
+ # This is escaping where the path to be rendered is being put into a ruby file which will
163
+ # later be read back by ruby (or something similar) so we need quadruple backslashes.
164
+ #
165
+ # In order to print:
166
+ #
167
+ # file_cache_path "C:\\chef"
168
+ #
169
+ # We need to convert "C:\chef" to "C:\\\\chef" to interpolate into a string which is rendered
170
+ # into the output file with that line in it.
171
+ #
172
+ # @param path [String] the path to escape
173
+ # @return [String] the escaped path
174
+ #
175
+ def self.escapepath(path)
176
+ path.gsub(BACKSLASH, BACKSLASH * 4)
177
+ end
178
+
179
+ def self.paths_eql?(path1, path2, windows: ChefUtils.windows?)
180
+ canonical_path(path1, windows: windows) == canonical_path(path2, windows: windows)
181
+ end
182
+
183
+ # @deprecated this method is deprecated. Please use escape_glob_dirs
184
+ # Paths which may contain glob-reserved characters need
185
+ # to be escaped before globbing can be done.
186
+ # http://stackoverflow.com/questions/14127343
187
+ def self.escape_glob(*parts, windows: ChefUtils.windows?)
188
+ path = cleanpath(join(*parts, windows: windows), windows: windows)
189
+ path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\" + x }
190
+ end
191
+
192
+ # This function does not switch to backslashes for windows
193
+ # This is because only forwardslashes should be used with dir (even for windows)
194
+ def self.escape_glob_dir(*parts)
195
+ path = Pathname.new(join(*parts)).cleanpath.to_s
196
+ path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\" + x }
197
+ end
198
+
199
+ def self.relative_path_from(from, to, windows: ChefUtils.windows?)
200
+ Pathname.new(cleanpath(to, windows: windows)).relative_path_from(Pathname.new(cleanpath(from, windows: windows)))
201
+ end
202
+
203
+ # Set the project-specific home directory environment variable.
204
+ #
205
+ # This can be used to allow per-tool home directory aliases like $KNIFE_HOME.
206
+ #
207
+ # @param [env_var] Key for an environment variable to use.
208
+ # @return [nil]
209
+ def self.per_tool_home_environment=(env_var)
210
+ @@per_tool_home_environment = env_var
211
+ # Reset this in case .home was already called.
212
+ @@home_dir = nil
213
+ end
214
+
215
+ # Retrieves the "home directory" of the current user while trying to ascertain the existence
216
+ # of said directory. The path returned uses / for all separators (the ruby standard format).
217
+ # If the home directory doesn't exist or an error is otherwise encountered, nil is returned.
218
+ #
219
+ # If a set of path elements is provided, they are appended as-is to the home path if the
220
+ # homepath exists.
221
+ #
222
+ # If an optional block is provided, the joined path is passed to that block if the home path is
223
+ # valid and the result of the block is returned instead.
224
+ #
225
+ # Home-path discovery is performed once. If a path is discovered, that value is memoized so
226
+ # that subsequent calls to home_dir don't bounce around.
227
+ #
228
+ # @see all_homes
229
+ # @param args [Array<String>] Path components to look for under the home directory.
230
+ # @return [String]
231
+ def self.home(*args)
232
+ @@home_dir ||= all_homes { |p| break p }
233
+ if @@home_dir
234
+ path = File.join(@@home_dir, *args)
235
+ block_given? ? (yield path) : path
236
+ end
237
+ end
238
+
239
+ # See self.home. This method performs a similar operation except that it yields all the different
240
+ # possible values of 'HOME' that one could have on this platform. Hence, on windows, if
241
+ # HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice.
242
+ # This method goes out and checks the existence of each location at the time of the call.
243
+ #
244
+ # The return is a list of all the returned values from each block invocation or a list of paths
245
+ # if no block is provided.
246
+ def self.all_homes(*args, windows: ChefUtils.windows?)
247
+ paths = []
248
+ paths << ENV[@@per_tool_home_environment] if defined?(@@per_tool_home_environment) && @@per_tool_home_environment && ENV[@@per_tool_home_environment]
249
+ paths << ENV["CHEF_HOME"] if ENV["CHEF_HOME"]
250
+ if windows
251
+ # By default, Ruby uses the the following environment variables to determine Dir.home:
252
+ # HOME
253
+ # HOMEDRIVE HOMEPATH
254
+ # USERPROFILE
255
+ # Ruby only checks to see if the variable is specified - not if the directory actually exists.
256
+ # On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive)
257
+ # while USERPROFILE points to the location where the user application settings and profile are stored. HOME
258
+ # is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is
259
+ # HOMESHARE instead of HOMEDRIVE.
260
+ #
261
+ # We instead walk down the following and only include paths that actually exist.
262
+ # HOME
263
+ # HOMEDRIVE HOMEPATH
264
+ # HOMESHARE HOMEPATH
265
+ # USERPROFILE
266
+
267
+ paths << ENV["HOME"]
268
+ paths << ENV["HOMEDRIVE"] + ENV["HOMEPATH"] if ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
269
+ paths << ENV["HOMESHARE"] + ENV["HOMEPATH"] if ENV["HOMESHARE"] && ENV["HOMEPATH"]
270
+ paths << ENV["USERPROFILE"]
271
+ end
272
+ paths << Dir.home if ENV["HOME"]
273
+
274
+ # Depending on what environment variables we're using, the slashes can go in any which way.
275
+ # Just change them all to / to keep things consistent.
276
+ # Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on
277
+ # the particular brand of kool-aid you consume. This code assumes that \ and / are both
278
+ # path separators on any system being used.
279
+ paths = paths.map { |home_path| home_path.gsub(path_separator(windows: windows), ::File::SEPARATOR) if home_path }
280
+
281
+ # Filter out duplicate paths and paths that don't exist.
282
+ valid_paths = paths.select { |home_path| home_path && Dir.exist?(home_path.force_encoding("utf-8")) }
283
+ valid_paths = valid_paths.uniq
284
+
285
+ # Join all optional path elements at the end.
286
+ # If a block is provided, invoke it - otherwise just return what we've got.
287
+ joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) }
288
+ if block_given?
289
+ joined_paths.each { |p| yield p }
290
+ else
291
+ joined_paths
292
+ end
293
+ end
294
+
295
+ # Determine if the given path is protected by macOS System Integrity Protection.
296
+ def self.is_sip_path?(path, node)
297
+ if ChefUtils.macos?
298
+ # @todo: parse rootless.conf for this?
299
+ sip_paths = [
300
+ "/System", "/bin", "/sbin", "/usr"
301
+ ]
302
+ sip_paths.each do |sip_path|
303
+ ChefConfig.logger.info("#{sip_path} is a SIP path, checking if it is in the exceptions list.")
304
+ return true if path.start_with?(sip_path)
305
+ end
306
+ false
307
+ else
308
+ false
309
+ end
310
+ end
311
+
312
+ # Determine if the given path is on the exception list for macOS System Integrity Protection.
313
+ def self.writable_sip_path?(path)
314
+ # todo: parse rootless.conf for this?
315
+ sip_exceptions = [
316
+ "/System/Library/Caches", "/System/Library/Extensions",
317
+ "/System/Library/Speech", "/System/Library/User Template",
318
+ "/usr/libexec/cups", "/usr/local", "/usr/share/man"
319
+ ]
320
+ sip_exceptions.each do |exception_path|
321
+ return true if path.start_with?(exception_path)
322
+ end
323
+ ChefConfig.logger.error("Cannot write to a SIP path #{path} on macOS!")
324
+ false
325
+ end
326
+
327
+ # Splits a string into an array of tokens as commands and arguments
328
+ #
329
+ # str = 'command with "some arguments"'
330
+ # split_args(str) => ["command", "with", "\"some arguments\""]
331
+ #
332
+ def self.split_args(line)
333
+ cmd_args = []
334
+ field = ""
335
+ line.scan(/\s*(?>([^\s\\"]+|"([^"]*)"|'([^']*)')|(\S))(\s|\z)?/m) do |word, within_dq, within_sq, esc, sep|
336
+
337
+ # Append the string with Word & Escape Character
338
+ field << (word || esc.gsub(/\\(.)/, "\\1"))
339
+
340
+ # Re-build the field when any whitespace character or
341
+ # End of string is encountered
342
+ if sep
343
+ cmd_args << field
344
+ field = ""
345
+ end
346
+ end
347
+ cmd_args
348
+ end
349
+ end
350
+ end