1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
#
# Author:: John Keiser (<jkeiser@opscode.com>)
# Copyright:: Copyright (c) 2012 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'chef/chef_fs'
require 'pathname'
class Chef
module ChefFS
class PathUtils
# A Chef-FS path is a path in a chef-repository that can be used to address
# both files on a local file-system as well as objects on a chef server.
# These paths are stricter than file-system paths allowed on various OSes.
# Absolute Chef-FS paths begin with "/" (on windows, "\" is acceptable as well).
# "/" is used as the path element separator (on windows, "\" is acceptable as well).
# No directory/path element may contain a literal "\" character. Any such characters
# encountered are either dealt with as separators (on windows) or as escape
# characters (on POSIX systems). Relative Chef-FS paths may use ".." or "." but
# may never use these to back-out of the root of a Chef-FS path. Any such extraneous
# ".."s are ignored.
# Chef-FS paths are case sensitive (since the paths on the server are).
# On OSes with case insensitive paths, you may be unable to locally deal with two
# objects whose server paths only differ by case. OTOH, the case of path segments
# that are outside the Chef-FS root (such as when looking at a file-system absolute
# path to discover the Chef-FS root path) are handled in accordance to the rules
# of the local file-system and OS.
def self.join(*parts)
return "" if parts.length == 0
# Determine if it started with a slash
absolute = parts[0].length == 0 || parts[0].length > 0 && parts[0] =~ /^#{regexp_path_separator}/
# Remove leading and trailing slashes from each part so that the join will work (and the slash at the end will go away)
parts = parts.map { |part| part.gsub(/^#{regexp_path_separator}+|#{regexp_path_separator}+$/, '') }
# Don't join empty bits
result = parts.select { |part| part != '' }.join('/')
# Put the / back on
absolute ? "/#{result}" : result
end
def self.split(path)
path.split(Regexp.new(regexp_path_separator))
end
def self.regexp_path_separator
Chef::ChefFS::windows? ? '[\/\\\\]' : '/'
end
# Given a server path, determines if it is absolute.
def self.is_absolute?(path)
!!(path =~ /^#{regexp_path_separator}/)
end
# Given a path which may only be partly real (i.e. /x/y/z when only /x exists,
# or /x/y/*/blah when /x/y/z/blah exists), call File.realpath on the biggest
# part that actually exists. The paths operated on here are not Chef-FS paths.
# These are OS paths that may contain symlinks but may not also fully exist.
#
# If /x is a symlink to /blarghle, and has no subdirectories, then:
# PathUtils.realest_path('/x/y/z') == '/blarghle/y/z'
# PathUtils.realest_path('/x/*/z') == '/blarghle/*/z'
# PathUtils.realest_path('/*/y/z') == '/*/y/z'
#
# TODO: Move this to wherever util/path_helper is these days.
def self.realest_path(path, cwd = Dir.pwd)
path = File.expand_path(path, cwd)
parent_path = File.dirname(path)
suffix = []
# File.dirname happens to return the path as its own dirname if you're
# at the root (such as at \\foo\bar, C:\ or /)
until parent_path == path do
# This can occur if a path such as "C:" is given. Ruby gives the parent as "C:."
# for reasons only it knows.
raise ArgumentError "Invalid path segment #{path}" if parent_path.length > path.length
begin
path = File.realpath(path)
break
rescue Errno::ENOENT
suffix << File.basename(path)
path = parent_path
parent_path = File.dirname(path)
end
end
File.join(path, *suffix.reverse)
end
# Compares two path fragments according to the case-sentitivity of the host platform.
def self.os_path_eq?(left, right)
Chef::ChefFS::windows? ? left.casecmp(right) == 0 : left == right
end
# Given two general OS-dependent file paths, determines the relative path of the
# child with respect to the ancestor. Both child and ancestor must exist and be
# fully resolved - this is strictly a lexical comparison. No trailing slashes
# and other shenanigans are allowed.
#
# TODO: Move this to util/path_helper.
def self.descendant_path(path, ancestor)
candidate_fragment = path[0, ancestor.length]
return nil unless PathUtils.os_path_eq?(candidate_fragment, ancestor)
if ancestor.length == path.length
''
elsif path[ancestor.length,1] =~ /#{PathUtils.regexp_path_separator}/
path[ancestor.length+1..-1]
else
nil
end
end
end
end
end
|