rubypath 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Path
4
+ class Backend
5
+ # rubocop:disable ClassLength
6
+ class Sys
7
+ def initialize(root = nil)
8
+ @root = ::File.expand_path root if root
9
+ @umask = File.umask
10
+ end
11
+
12
+ def quit
13
+ File.umask @umask
14
+ end
15
+
16
+ def home(user)
17
+ ::File.expand_path "~#{user}"
18
+ end
19
+
20
+ def getwd
21
+ ::Dir.getwd
22
+ end
23
+
24
+ def user
25
+ require 'etc'
26
+
27
+ Etc.getlogin
28
+ end
29
+
30
+ def r(path)
31
+ return path unless @root
32
+ ::File.expand_path("#{@root}/#{::File.expand_path(path)}")
33
+ end
34
+
35
+ def ur(path)
36
+ return path unless @root
37
+
38
+ if path.slice(0, @root.length) == @root
39
+ path.slice(@root.length, path.length - @root.length)
40
+ else
41
+ path
42
+ end
43
+ end
44
+
45
+ def fs(path, obj, method, *args)
46
+ # puts "[FS] #{obj} #{method} #{args.inspect}"
47
+ obj.send method, *args
48
+ rescue Errno::ENOENT
49
+ raise Errno::ENOENT.new path
50
+ rescue Errno::EISDIR
51
+ raise Errno::EISDIR.new path
52
+ rescue Errno::ENOTDIR
53
+ raise Errno::ENOTDIR.new path
54
+ rescue Errno::EACCES
55
+ raise Errno::EACCES.new path
56
+ end
57
+
58
+ ## OPERATIONS
59
+
60
+ def expand_path(path, base)
61
+ ::File.expand_path path, base
62
+ end
63
+
64
+ def exists?(path)
65
+ fs path, ::File, :exists?, r(path)
66
+ end
67
+
68
+ def mkdir(path)
69
+ fs path, ::Dir, :mkdir, r(path)
70
+ end
71
+
72
+ def mkpath(path)
73
+ fs path, ::FileUtils, :mkdir_p, r(path)
74
+ end
75
+
76
+ def directory?(path)
77
+ fs path, ::File, :directory?, r(path)
78
+ end
79
+
80
+ def file?(path)
81
+ fs path, ::File, :file?, r(path)
82
+ end
83
+
84
+ def touch(path)
85
+ fs path, ::FileUtils, :touch, r(path)
86
+ end
87
+
88
+ def write(path, content, *args)
89
+ fs path, ::IO, :write, r(path), content, *args
90
+ end
91
+
92
+ def read(path, *args)
93
+ fs path, ::IO, :read, r(path), *args
94
+ end
95
+
96
+ def mtime(path)
97
+ fs path, ::File, :mtime, r(path)
98
+ end
99
+
100
+ def mtime=(path, time)
101
+ fs path, ::File, :utime, atime(path), time, r(path)
102
+ end
103
+
104
+ def atime(path)
105
+ fs path, ::File, :atime, r(path)
106
+ end
107
+
108
+ def atime=(path, time)
109
+ fs path, ::File, :utime, time, mtime(path), r(path)
110
+ end
111
+
112
+ def entries(path)
113
+ fs path, ::Dir, :entries, r(path)
114
+ end
115
+
116
+ def glob(pattern, flags = 0)
117
+ if block_given?
118
+ fs pattern, ::Dir, :glob, r(pattern), flags do |path|
119
+ yield ur(path)
120
+ end
121
+ else
122
+ fs(pattern, ::Dir, :glob, r(pattern), flags).map {|path| ur path }
123
+ end
124
+ end
125
+
126
+ def umask
127
+ File.umask
128
+ end
129
+
130
+ def umask=(mask)
131
+ File.umask mask
132
+ end
133
+
134
+ def mode(path)
135
+ fs(path, ::File, :stat, r(path)).mode & 0o777
136
+ end
137
+
138
+ def chmod(path, mode)
139
+ fs path, ::File, :chmod, mode, r(path)
140
+ end
141
+
142
+ def unlink(path)
143
+ fs path, ::File, :unlink, r(path)
144
+ end
145
+
146
+ def rmtree(path)
147
+ fs path, ::FileUtils, :rm_r, r(path), force: true
148
+ end
149
+
150
+ def rmtree!(path)
151
+ fs path, ::FileUtils, :rm_r, r(path)
152
+ end
153
+
154
+ def safe_rmtree(path)
155
+ fs path, ::FileUtils, :rm_r, r(path), force: true, secure: true
156
+ end
157
+
158
+ def safe_rmtree!(path)
159
+ fs path, ::FileUtils, :rm_r, r(path), secure: true
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Path
4
+ # @!group Comparison
5
+
6
+ # Compare path to given object. If object is a string, Path or #{Path.like?}
7
+ # they will be compared using the string paths. Otherwise they are assumed
8
+ # as not equal.
9
+ #
10
+ # @param other [Object] Object to compare path with.
11
+ # @return [Boolean] True if object represents same path.
12
+ #
13
+ def eql?(other)
14
+ if other.is_a?(Path)
15
+ cleanpath.internal_path == other.cleanpath.internal_path
16
+ elsif Path.like?(other)
17
+ Path.new(other).eql?(self)
18
+ end
19
+ end
20
+ alias == eql?
21
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Path
4
+ class << self
5
+ # @!group Construction
6
+
7
+ # Create new {Path}.
8
+ #
9
+ # If single argument is a path object it will be returned and no new one
10
+ # will be created. If not arguments are given {Path::EMPTY} will be
11
+ # returned.
12
+ #
13
+ # @see #initialize
14
+ #
15
+ def new(*args)
16
+ args.flatten!
17
+ return Path::EMPTY if args.empty?
18
+ return args.first if args.size == 1 && args.first.is_a?(self)
19
+ super
20
+ end
21
+
22
+ # Check if given object is like a path.
23
+ #
24
+ # An object is like a path if
25
+ # 1. It is a {Path} object.
26
+ # 2. It is a string.
27
+ # 3. It responds to {#to_path} and {#to_path} returns a string.
28
+ # 4. It responds to {#path} and {#path} returns a string.
29
+ #
30
+ # If no rule matches it is not considered to be like a path.
31
+ #
32
+ # @return [Boolean] True if object is path like, false otherwise.
33
+ #
34
+ # rubocop:disable Metrics/CyclomaticComplexity
35
+ #
36
+ def like?(obj)
37
+ return true if obj.is_a?(self)
38
+ return true if obj.is_a?(String)
39
+ return true if obj.respond_to?(:to_path) && obj.to_path.is_a?(String)
40
+ return true if obj.respond_to?(:path) && obj.path.is_a?(String)
41
+ false
42
+ end
43
+ # rubocop:enable all
44
+
45
+ # Convert given object to path string using {::Path.like?} rules.
46
+ #
47
+ # @note Should not be used directly.
48
+ #
49
+ # @return [String]
50
+ # @raise [ArgumentError] If given object is not {::Path.like?}.
51
+ # @see ::Path.like?
52
+ #
53
+ # rubocop:disable Metrics/MethodLength
54
+ def like_path(obj)
55
+ case obj
56
+ when String
57
+ return obj
58
+ else
59
+ %i[to_path path to_str to_s].each do |mth|
60
+ if obj.respond_to?(mth) && obj.send(mth).is_a?(String)
61
+ return obj.send(mth)
62
+ end
63
+ end
64
+ end
65
+
66
+ raise ArgumentError.new \
67
+ "Argument #{obj.inspect} cannot be converted to path string."
68
+ end
69
+ # rubocop:enable all
70
+
71
+ # Return system file path separator.
72
+ #
73
+ # @return [String] File separator.
74
+ # @see ::File::SEPARATOR
75
+ #
76
+ def separator
77
+ ::File::SEPARATOR
78
+ end
79
+
80
+ # Allow class object to be used as a bock.
81
+ #
82
+ # @example
83
+ # %w(path/to/fileA path/to/fileB).map(&Path)
84
+ #
85
+ def to_proc
86
+ proc {|*args| Path.new(*args) }
87
+ end
88
+ end
89
+
90
+ # @!group Construction
91
+
92
+ # Initialize new {Path} object.
93
+ #
94
+ # Given arguments will be converted to String using `#to_path`, `#path` or
95
+ # `#to_s` in this order if they return a String object.
96
+ #
97
+ # @overload initialize([[String, #to_path, #path, #to_s], ...]
98
+ #
99
+ def initialize(*args)
100
+ parts = args.flatten
101
+ @path = if parts.size > 1
102
+ ::File.join(*parts.map {|p| Path.like_path p })
103
+ elsif parts.size == 1
104
+ Path.like_path(parts.first).dup
105
+ else
106
+ ''
107
+ end
108
+ end
109
+
110
+ # Empty path.
111
+ #
112
+ # @return [Path] Empty path.
113
+ #
114
+ EMPTY = Path.new('')
115
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Path
4
+ class << self
5
+ # Returns the current working directory.
6
+ #
7
+ # @return [Path] Current working directory.
8
+ # @see ::Dir.getwd
9
+ #
10
+ def getwd
11
+ new Backend.instance.getwd
12
+ end
13
+
14
+ def glob(pattern, flags = nil)
15
+ flags = default_glob_flags(flags)
16
+
17
+ if block_given?
18
+ Backend.instance.glob(pattern, flags) {|path| yield Path path }
19
+ else
20
+ Backend.instance.glob(pattern, flags).map(&Path)
21
+ end
22
+ end
23
+
24
+ # @!visibility private
25
+ #
26
+ def default_glob_flags(flags)
27
+ if flags.nil? && defined?(::File::FNM_EXTGLOB)
28
+ ::File::FNM_EXTGLOB
29
+ else
30
+ flags.to_i
31
+ end
32
+ end
33
+ end
34
+
35
+ # @!group Directory Operations
36
+
37
+ # Create directory.
38
+ #
39
+ # Given arguments will be joined with current path before directory is
40
+ # created.
41
+ #
42
+ # @raise [Errno::ENOENT] If parent directory could not created.
43
+ # @return [Path] Path to created directory.
44
+ # @see #mkpath
45
+ #
46
+ def mkdir(*args)
47
+ with_path(*args) do |path|
48
+ Backend.instance.mkdir path
49
+ Path path
50
+ end
51
+ end
52
+
53
+ # Create directory and all missing parent directories.
54
+ #
55
+ # Given arguments will be joined with current path before directories
56
+ # are created.
57
+ #
58
+ # @return [Path] Path to created directory.
59
+ # @see #mkdir
60
+ # @see ::FileUtils.mkdir_p
61
+ #
62
+ def mkpath(*args)
63
+ with_path(*args) do |path|
64
+ Backend.instance.mkpath path
65
+ Path path
66
+ end
67
+ end
68
+ alias mkdir_p mkpath
69
+
70
+ # Return list of entries in directory. That includes special directories
71
+ # (`.`, `..`).
72
+ #
73
+ # Given arguments will be joined before children are listed for directory.
74
+ #
75
+ # @return [Array<Path>] Entries in directory.
76
+ #
77
+ def entries(*_args)
78
+ invoke_backend(:entries, internal_path).map(&Path)
79
+ end
80
+
81
+ #
82
+ def glob(pattern, flags = nil, &block)
83
+ Path.glob(::File.join(escaped_glob_path, pattern), flags, &block)
84
+ end
85
+
86
+ # Removes file or directory. If it's a directory it will be removed
87
+ # recursively.
88
+ #
89
+ # WARNING: This method causes local vulnerability if one of parent
90
+ # directories or removing directory tree are world writable (including
91
+ # `/tmp`, whose permission is 1777), and the current process has strong
92
+ # privilege such as Unix super user (root), and the system has symbolic link.
93
+ # For secure removing see {#safe_rmtree}.
94
+ #
95
+ # @return [Path] Path to removed file or directory.
96
+ #
97
+ def rmtree(*args)
98
+ with_path(*args) do |path|
99
+ invoke_backend :rmtree, internal_path
100
+ Path path
101
+ end
102
+ end
103
+ alias rm_rf rmtree
104
+
105
+ # Removes file or directory. If it's a directory it will be removed
106
+ # recursively.
107
+ #
108
+ # This method uses #{FileUtils#remove_entry_secure} to avoid TOCTTOU
109
+ # (time-of-check-to-time-of-use) local security vulnerability of {#rmtree}.
110
+ # {#rmtree} causes security hole when:
111
+ #
112
+ # * Parent directory is world writable (including `/tmp`).
113
+ # * Removing directory tree includes world writable directory.
114
+ # * The system has symbolic link.
115
+ #
116
+ # @return [Path] Path to removed file or directory.
117
+ #
118
+ def safe_rmtree(*args)
119
+ with_path(*args) do |path|
120
+ invoke_backend :safe_rmtree, internal_path
121
+ Path path
122
+ end
123
+ end
124
+
125
+ # Removes file or directory. If it's a directory it will be removed
126
+ # recursively.
127
+ #
128
+ # This method behaves exactly like {#rmtree} but will raise exceptions
129
+ # e.g. when file does not exist.
130
+ #
131
+ # @return [Path] Path to removed file or directory.
132
+ #
133
+ def rmtree!(*args)
134
+ with_path(*args) do |path|
135
+ invoke_backend :rmtree!, internal_path
136
+ Path path
137
+ end
138
+ end
139
+ alias rm_r rmtree!
140
+
141
+ # Removes file or directory. If it's a directory it will be removed
142
+ # recursively.
143
+ #
144
+ # This method behaves exactly like {#safe_rmtree} but will raise exceptions
145
+ # e.g. when file does not exist.
146
+ #
147
+ # @return [Path] Path to removed file or directory.
148
+ #
149
+ def safe_rmtree!(*args)
150
+ with_path(*args) do |path|
151
+ invoke_backend :safe_rmtree!, internal_path
152
+ Path path
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def escaped_glob_path
159
+ internal_path.gsub(/[\[\]\*\?\{\}]/, '\\\\\0')
160
+ end
161
+ end