summaryrefslogtreecommitdiff
path: root/lib/chef/chef_fs/path_utils.rb
blob: b8a83ab09f40d253a0edd8a54d4b7b7c18fb0ab6 (plain)
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
127
128
129
130
#
# Author:: John Keiser (<jkeiser@chef.io>)
# Copyright:: Copyright (c) Chef Software 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_relative "../chef_fs"
require "pathname" unless defined?(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
        ChefUtils.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
          # 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, Errno::EINVAL
            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-sensitivity of the host platform.
      def self.os_path_eq?(left, right)
        ChefUtils.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