rubypath 1.0.0 → 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.
@@ -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