memfs 1.0.0 → 2.0.0

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.
data/lib/memfs/dir.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
1
4
  require 'memfs/filesystem_access'
2
5
 
3
6
  module MemFs
@@ -6,6 +9,8 @@ module MemFs
6
9
  include Enumerable
7
10
  include FilesystemAccess
8
11
 
12
+ Tmpname = MemFs::OriginalDir::Tmpname
13
+
9
14
  attr_reader :pos
10
15
 
11
16
  def self.[](*patterns)
@@ -17,15 +22,26 @@ module MemFs
17
22
  0
18
23
  end
19
24
 
25
+ if MemFs.ruby_version_gte?('2.6')
26
+ def self.children(dirname, opts = {})
27
+ entries(dirname, opts) - %w[. ..]
28
+ end
29
+ end
30
+
20
31
  def self.chroot(path)
21
32
  fail Errno::EPERM, path unless Process.uid.zero?
22
33
 
23
34
  dir = fs.find_directory!(path)
24
- dir.name = '/'
35
+ dir.name = MemFs.platform_root
25
36
  fs.root = dir
26
37
  0
27
38
  end
28
39
 
40
+ def self.empty?(path)
41
+ entry = fs.find!(path)
42
+ File.directory?(path) && entry.empty?
43
+ end
44
+
29
45
  def self.entries(dirname, _opts = {})
30
46
  fs.entries(dirname)
31
47
  end
@@ -33,7 +49,7 @@ module MemFs
33
49
  def self.exists?(path)
34
50
  File.directory?(path)
35
51
  end
36
- class << self; alias_method :exist?, :exists?; end
52
+ class << self; alias exist? exists?; end
37
53
 
38
54
  def self.foreach(dirname, &block)
39
55
  return to_enum(__callee__, dirname) unless block
@@ -44,27 +60,44 @@ module MemFs
44
60
  def self.getwd
45
61
  fs.getwd
46
62
  end
47
- class << self; alias_method :pwd, :getwd; end
63
+ class << self; alias pwd getwd; end
48
64
 
49
- def self.glob(patterns, flags = 0)
50
- patterns = [*patterns].map(&:to_s)
65
+ # rubocop:disable Lint/UnderscorePrefixedVariableName, Lint/UnusedMethodArgument
66
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
67
+ # rubocop:disable Metrics/PerceivedComplexity
68
+ def self.glob(patterns, _flags = 0, flags: _flags, base: nil, sort: true, &block)
69
+ # rubocop:enable Lint/UnderscorePrefixedVariableName, Lint/UnusedMethodArgument
70
+ original_patterns = [*patterns].map(&:to_s)
71
+ # Normalize patterns for platform (e.g., '/test' -> 'D:/test' on Windows)
72
+ normalized_patterns = original_patterns.map { |p| MemFs.normalize_path(p) }
51
73
  list = fs.paths.select do |path|
52
- patterns.any? do |pattern|
74
+ normalized_patterns.any? do |pattern|
53
75
  File.fnmatch?(pattern, path, flags | GLOB_FLAGS)
54
76
  end
55
77
  end
56
- # FIXME: ugly special case for /* and /
57
- list.delete('/') if patterns.first == '/*'
78
+
79
+ # Special case for /* and /
80
+ # A scenario where /* is not the only pattern and / should be returned is
81
+ # considered an edge-case (platform-aware root handling).
82
+ root_pattern = MemFs.windows? ? "#{MemFs.platform_root}*" : '/*'
83
+ if ['/*', root_pattern].include?(original_patterns.first)
84
+ list.delete(MemFs.platform_root)
85
+ list.delete('/') # Also handle Unix-style if passed
86
+ end
87
+
58
88
  return list unless block_given?
59
- list.each { |path| yield path }
89
+
90
+ list.each(&block)
60
91
  nil
61
92
  end
93
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
94
+ # rubocop:enable Metrics/PerceivedComplexity
62
95
 
63
96
  def self.home(*args)
64
97
  original_dir_class.home(*args)
65
98
  end
66
99
 
67
- def self.mkdir(path, mode = 0777)
100
+ def self.mkdir(path, mode = 0o777)
68
101
  fs.mkdir path, mode
69
102
  end
70
103
 
@@ -77,7 +110,7 @@ module MemFs
77
110
  dir
78
111
  end
79
112
  ensure
80
- dir && dir.close if block_given?
113
+ dir&.close if block_given?
81
114
  end
82
115
 
83
116
  def self.rmdir(path)
@@ -85,12 +118,37 @@ module MemFs
85
118
  end
86
119
 
87
120
  def self.tmpdir
88
- '/tmp'
121
+ File.join(MemFs.platform_root, 'tmp')
122
+ end
123
+
124
+ # rubocop:disable Metrics/MethodLength
125
+ def self.mktmpdir(prefix_suffix = nil, tmpdir = nil, **options)
126
+ tmpdir = MemFs.normalize_path(tmpdir || self.tmpdir)
127
+ path = MemFs::OriginalDir::Tmpname.create(
128
+ prefix_suffix || 'd',
129
+ tmpdir,
130
+ **options) { |p, _, _, _d| mkdir(p, 0o700) }
131
+
132
+ # Normalize the returned path for consistent handling across platforms
133
+ path = MemFs.normalize_path(path)
134
+
135
+ return path unless block_given?
136
+
137
+ begin
138
+ yield path.dup
139
+ ensure
140
+ begin
141
+ rmdir(path) if exists?(path)
142
+ rescue StandardError
143
+ # Ignore cleanup errors
144
+ end
145
+ end
89
146
  end
147
+ # rubocop:enable Metrics/MethodLength
90
148
 
91
149
  class << self
92
- alias_method :delete, :rmdir
93
- alias_method :unlink, :rmdir
150
+ alias delete rmdir
151
+ alias unlink rmdir
94
152
  end
95
153
 
96
154
  def initialize(path)
@@ -110,15 +168,21 @@ module MemFs
110
168
  entry.entry_names.each(&block)
111
169
  end
112
170
 
171
+ def fileno
172
+ entry.fileno
173
+ end
174
+
113
175
  def path
114
176
  entry.path
115
177
  end
116
- alias_method :to_path, :path
178
+ alias to_path path
117
179
 
180
+ # rubocop:disable Lint/Void
118
181
  def pos=(position)
119
182
  seek(position)
120
183
  position
121
184
  end
185
+ # rubocop:enable Lint/Void
122
186
 
123
187
  def read
124
188
  name = entries[pos]
@@ -141,13 +205,14 @@ module MemFs
141
205
  @pos
142
206
  end
143
207
 
144
- private
145
-
146
208
  GLOB_FLAGS = if defined?(File::FNM_EXTGLOB)
147
209
  File::FNM_EXTGLOB | File::FNM_PATHNAME
148
210
  else
149
211
  File::FNM_PATHNAME
150
212
  end
213
+ private_constant :GLOB_FLAGS
214
+
215
+ private
151
216
 
152
217
  attr_accessor :entry, :max_seek, :state
153
218
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'memfs/fake/entry'
2
4
 
3
5
  module MemFs
@@ -18,8 +20,20 @@ module MemFs
18
20
  entries.keys
19
21
  end
20
22
 
23
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
21
24
  def find(path)
22
- path = path.sub(%r{\A/+}, '').sub(%r{/+\z}, '')
25
+ path = MemFs.normalize_path(path)
26
+
27
+ # Strip root prefix if present (platform_root first, then directory name for root dirs)
28
+ if path.start_with?(MemFs.platform_root)
29
+ path = path[MemFs.platform_root.length..]
30
+ elsif root_directory? && path.start_with?(name)
31
+ path = path[name.length..]
32
+ end
33
+
34
+ path = path.gsub(%r{(\A/+|/+\z)}, '')
35
+ return self if path.empty?
36
+
23
37
  parts = path.split('/', 2)
24
38
 
25
39
  if entry_names.include?(path)
@@ -28,6 +42,7 @@ module MemFs
28
42
  entries[parts.first].find(parts.last)
29
43
  end
30
44
  end
45
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
31
46
 
32
47
  def initialize(*args)
33
48
  super
@@ -40,14 +55,18 @@ module MemFs
40
55
  end
41
56
 
42
57
  def path
43
- name == '/' ? '/' : super
58
+ root_directory? ? name : super
44
59
  end
45
60
 
46
61
  def paths
47
- [path] + entries.reject { |p| %w[. ..].include?(p) }
48
- .values
49
- .map(&:paths)
50
- .flatten
62
+ current_or_parent_dirs = %w[. ..]
63
+
64
+ [path] +
65
+ entries
66
+ .reject { current_or_parent_dirs.include?(_1) }
67
+ .values
68
+ .map(&:paths)
69
+ .flatten
51
70
  end
52
71
 
53
72
  def remove_entry(entry)
@@ -57,6 +76,12 @@ module MemFs
57
76
  def type
58
77
  'directory'
59
78
  end
79
+
80
+ private
81
+
82
+ def root_directory?
83
+ parent.nil? || MemFs.root_path?(name)
84
+ end
60
85
  end
61
86
  end
62
87
  end
@@ -1,29 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MemFs
2
4
  module Fake
3
5
  class Entry
4
- UREAD = 00100
5
- UWRITE = 00200
6
- UEXEC = 00400
7
- GREAD = 00010
8
- GWRITE = 00020
9
- GEXEC = 00040
10
- OREAD = 00001
11
- OWRITE = 00002
12
- OEXEC = 00004
13
- RSTICK = 01000
14
- USTICK = 05000
15
- SETUID = 04000
16
- SETGID = 02000
6
+ UREAD = 0o0400
7
+ UWRITE = 0o0200
8
+ UEXEC = 0o0100
9
+ GREAD = 0o0040
10
+ GWRITE = 0o0020
11
+ GEXEC = 0o0010
12
+ OREAD = 0o0004
13
+ OWRITE = 0o0002
14
+ OEXEC = 0o0001
15
+ RSTICK = 0o1000
16
+ USTICK = 0o5000
17
+ SETUID = 0o4000
18
+ SETGID = 0o2000
17
19
 
18
20
  attr_accessor :atime,
19
- :block_device,
20
- :character_device,
21
- :ctime,
22
- :gid,
23
- :mtime,
24
- :name,
25
- :parent,
26
- :uid
21
+ :birthtime,
22
+ :block_device,
23
+ :character_device,
24
+ :ctime,
25
+ :gid,
26
+ :mtime,
27
+ :name,
28
+ :parent,
29
+ :uid
27
30
  attr_reader :mode
28
31
 
29
32
  def blksize
@@ -50,31 +53,44 @@ module MemFs
50
53
  @dev ||= rand(1000)
51
54
  end
52
55
 
56
+ def fileno
57
+ fail NotImplementedError
58
+ end
59
+
53
60
  def find(_path)
54
61
  fail Errno::ENOTDIR, path
55
62
  end
56
63
 
64
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
57
65
  def initialize(path = nil)
58
66
  time = Time.now
59
67
  self.atime = time
68
+ self.birthtime = time
60
69
  self.ctime = time
61
70
  self.gid = Process.egid
62
- self.mode = 0666 - MemFs::File.umask
71
+ self.mode = 0o666 - MemFs::File.umask
63
72
  self.mtime = time
64
- self.name = MemFs::File.basename(path || '')
73
+ # Preserve full path for root directories (e.g., 'D:/' on Windows)
74
+ # since File.basename('D:/') returns '/' which breaks path matching
75
+ self.name = if path && MemFs.root_path?(path)
76
+ MemFs.normalize_path(path)
77
+ else
78
+ MemFs::File.basename(path || '')
79
+ end
65
80
  self.uid = Process.euid
66
81
  end
82
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
67
83
 
68
84
  def ino
69
85
  @ino ||= rand(1000)
70
86
  end
71
87
 
72
88
  def mode=(mode_int)
73
- @mode = 0100000 | mode_int
89
+ @mode = 0o100000 | mode_int
74
90
  end
75
91
 
76
92
  def path
77
- parts = [parent && parent.path, name].compact
93
+ parts = [parent&.path, name].compact
78
94
  MemFs::File.join(parts)
79
95
  end
80
96
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
 
3
5
  module MemFs
@@ -6,10 +8,11 @@ module MemFs
6
8
  class Content < SimpleDelegator
7
9
  attr_accessor :pos
8
10
 
9
- def close
10
- end
11
+ def close; end
11
12
 
12
13
  def initialize(obj = '')
14
+ super
15
+
13
16
  @string = obj.to_s.dup
14
17
  @pos = 0
15
18
 
@@ -24,7 +27,7 @@ module MemFs
24
27
  end
25
28
  end
26
29
 
27
- def read(length = nil, buffer = '')
30
+ def read(length = nil, buffer = +'')
28
31
  length ||= @string.length - @pos
29
32
  buffer.replace @string[@pos, length]
30
33
  @pos += buffer.bytesize
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'memfs/fake/entry'
2
4
  require 'memfs/fake/file/content'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'memfs/filesystem_access'
2
4
 
3
5
  module MemFs
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'memfs/filesystem_access'
3
5
 
@@ -10,15 +12,16 @@ module MemFs
10
12
  attr_reader :entry
11
13
 
12
14
  def_delegators :entry,
13
- :atime,
14
- :blksize,
15
- :ctime,
16
- :dev,
17
- :gid,
18
- :ino,
19
- :mode,
20
- :mtime,
21
- :uid
15
+ :atime,
16
+ :birthtime,
17
+ :blksize,
18
+ :ctime,
19
+ :dev,
20
+ :gid,
21
+ :ino,
22
+ :mode,
23
+ :mtime,
24
+ :uid
22
25
 
23
26
  def blockdev?
24
27
  !!entry.block_device
@@ -52,11 +55,15 @@ module MemFs
52
55
  gid == Process.egid
53
56
  end
54
57
 
55
- def initialize(path, dereference = false)
58
+ def initialize(path, dereference: false)
56
59
  entry = fs.find!(path)
57
60
  @entry = dereference ? entry.dereferenced : entry
58
61
  end
59
62
 
63
+ def nlink
64
+ directory? ? 2 : 1
65
+ end
66
+
60
67
  def owned?
61
68
  uid == Process.euid
62
69
  end
@@ -94,11 +101,11 @@ module MemFs
94
101
  end
95
102
 
96
103
  def world_readable?
97
- entry.mode - 0100000 if (entry.mode & Fake::Entry::OREAD).nonzero?
104
+ entry.mode - 0o100000 if (entry.mode & Fake::Entry::OREAD).nonzero?
98
105
  end
99
106
 
100
107
  def world_writable?
101
- entry.mode - 0100000 if (entry.mode & Fake::Entry::OWRITE).nonzero?
108
+ entry.mode - 0o100000 if (entry.mode & Fake::Entry::OWRITE).nonzero?
102
109
  end
103
110
 
104
111
  def writable?
@@ -164,7 +171,7 @@ module MemFs
164
171
  end
165
172
 
166
173
  def world_executable?
167
- entry.mode - 0100000 if (entry.mode & Fake::Entry::OEXEC).nonzero?
174
+ entry.mode - 0o100000 if (entry.mode & Fake::Entry::OEXEC).nonzero?
168
175
  end
169
176
  end
170
177
  end