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.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +19 -0
- data/.github/workflows/ci.yml +64 -0
- data/.rspec +1 -1
- data/.rubocop.yml +98 -0
- data/CHANGELOG.md +30 -10
- data/Gemfile +19 -0
- data/Guardfile +5 -3
- data/README.md +3 -3
- data/Rakefile +4 -2
- data/bin/_guard-core +16 -0
- data/bin/coverage_summary +51 -0
- data/bin/guard +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/bin/rubocop +16 -0
- data/lib/memfs/dir.rb +82 -17
- data/lib/memfs/fake/directory.rb +31 -6
- data/lib/memfs/fake/entry.rb +41 -25
- data/lib/memfs/fake/file/content.rb +6 -3
- data/lib/memfs/fake/file.rb +2 -0
- data/lib/memfs/fake/symlink.rb +2 -0
- data/lib/memfs/file/stat.rb +20 -13
- data/lib/memfs/file.rb +74 -66
- data/lib/memfs/file_system.rb +14 -9
- data/lib/memfs/filesystem_access.rb +2 -0
- data/lib/memfs/io.rb +48 -54
- data/lib/memfs/version.rb +3 -1
- data/lib/memfs.rb +83 -3
- data/memfs.gemspec +0 -17
- data/spec/fileutils_spec.rb +64 -41
- data/spec/memfs/dir_spec.rb +155 -18
- data/spec/memfs/fake/directory_spec.rb +3 -3
- data/spec/memfs/fake/entry_spec.rb +11 -5
- data/spec/memfs/fake/symlink_spec.rb +1 -1
- data/spec/memfs/file/stat_spec.rb +27 -15
- data/spec/memfs/file_spec.rb +135 -51
- data/spec/memfs/file_system_spec.rb +15 -15
- data/spec/memfs_spec.rb +29 -1
- data/spec/spec_helper.rb +19 -4
- metadata +17 -151
- data/.hound.yml +0 -2
- data/.rubocop.yml +0 -1
- data/.ruby-style.yml +0 -335
- data/.travis.yml +0 -9
data/lib/memfs/file.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'forwardable'
|
|
2
4
|
require 'memfs/filesystem_access'
|
|
3
5
|
require 'memfs/io'
|
|
@@ -10,50 +12,51 @@ module MemFs
|
|
|
10
12
|
include Enumerable
|
|
11
13
|
include FilesystemAccess
|
|
12
14
|
|
|
15
|
+
PATH_SEPARATOR = '/'
|
|
13
16
|
ALT_SEPARATOR = nil
|
|
14
17
|
|
|
15
18
|
MODE_MAP = {
|
|
16
|
-
'r'
|
|
19
|
+
'r' => RDONLY,
|
|
17
20
|
'r+' => RDWR,
|
|
18
|
-
'w'
|
|
21
|
+
'w' => CREAT | TRUNC | WRONLY,
|
|
19
22
|
'w+' => CREAT | TRUNC | RDWR,
|
|
20
|
-
'a'
|
|
23
|
+
'a' => CREAT | APPEND | WRONLY,
|
|
21
24
|
'a+' => CREAT | APPEND | RDWR
|
|
22
25
|
}.freeze
|
|
23
26
|
|
|
24
|
-
SEPARATOR = '/'
|
|
27
|
+
SEPARATOR = '/'
|
|
25
28
|
SUCCESS = 0
|
|
26
29
|
|
|
27
30
|
@umask = nil
|
|
28
31
|
|
|
29
32
|
def_delegators :original_file_class,
|
|
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
|
-
|
|
33
|
+
:basename,
|
|
34
|
+
:dirname,
|
|
35
|
+
:extname,
|
|
36
|
+
:fnmatch,
|
|
37
|
+
:join,
|
|
38
|
+
:path,
|
|
39
|
+
:split
|
|
40
|
+
|
|
41
|
+
%i[
|
|
42
|
+
blockdev?
|
|
43
|
+
chardev?
|
|
44
|
+
directory?
|
|
45
|
+
executable?
|
|
46
|
+
executable_real?
|
|
47
|
+
file?
|
|
48
|
+
grpowned?
|
|
49
|
+
owned?
|
|
50
|
+
pipe?
|
|
51
|
+
readable?
|
|
52
|
+
readable_real?
|
|
53
|
+
setgid?
|
|
54
|
+
setuid?
|
|
55
|
+
socket?
|
|
56
|
+
sticky?
|
|
57
|
+
writable?
|
|
58
|
+
writable_real?
|
|
59
|
+
zero?
|
|
57
60
|
].each do |query_method|
|
|
58
61
|
# def directory?(path)
|
|
59
62
|
# stat_query(path, :directory?)
|
|
@@ -63,15 +66,17 @@ module MemFs
|
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
class << self; alias empty? zero?; end
|
|
70
|
+
|
|
71
|
+
%i[
|
|
72
|
+
world_readable?
|
|
73
|
+
world_writable?
|
|
69
74
|
].each do |query_method|
|
|
70
75
|
# def directory?(path)
|
|
71
76
|
# stat_query(path, :directory?, false)
|
|
72
77
|
# end
|
|
73
78
|
define_singleton_method(query_method) do |path|
|
|
74
|
-
stat_query(path, query_method, false)
|
|
79
|
+
stat_query(path, query_method, force_boolean: false)
|
|
75
80
|
end
|
|
76
81
|
end
|
|
77
82
|
|
|
@@ -83,6 +88,10 @@ module MemFs
|
|
|
83
88
|
stat(path).atime
|
|
84
89
|
end
|
|
85
90
|
|
|
91
|
+
def self.birthtime(path)
|
|
92
|
+
stat(path).birthtime
|
|
93
|
+
end
|
|
94
|
+
|
|
86
95
|
def self.chmod(mode_int, *paths)
|
|
87
96
|
paths.each do |path|
|
|
88
97
|
fs.chmod mode_int, path
|
|
@@ -103,7 +112,7 @@ module MemFs
|
|
|
103
112
|
def self.exists?(path)
|
|
104
113
|
!!fs.find(path)
|
|
105
114
|
end
|
|
106
|
-
class << self;
|
|
115
|
+
class << self; alias exist? exists?; end
|
|
107
116
|
|
|
108
117
|
def self.expand_path(file_name, dir_string = fs.pwd)
|
|
109
118
|
original_file_class.expand_path(file_name, dir_string)
|
|
@@ -113,10 +122,12 @@ module MemFs
|
|
|
113
122
|
fs.find!(path) && lstat(path).ftype
|
|
114
123
|
end
|
|
115
124
|
|
|
116
|
-
class << self;
|
|
125
|
+
class << self; alias fnmatch? fnmatch; end
|
|
117
126
|
|
|
118
127
|
def self.identical?(path1, path2)
|
|
119
|
-
|
|
128
|
+
path1 = path1.path if path1.respond_to?(:path)
|
|
129
|
+
path2 = path2.path if path2.respond_to?(:path)
|
|
130
|
+
fs.find!(path1).dereferenced.equal? fs.find!(path2).dereferenced
|
|
120
131
|
rescue Errno::ENOENT
|
|
121
132
|
false
|
|
122
133
|
end
|
|
@@ -183,15 +194,13 @@ module MemFs
|
|
|
183
194
|
|
|
184
195
|
def self.size?(path)
|
|
185
196
|
file = fs.find(path)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
false
|
|
190
|
-
end
|
|
197
|
+
size = file&.size.to_i
|
|
198
|
+
|
|
199
|
+
size.positive? ? size : false
|
|
191
200
|
end
|
|
192
201
|
|
|
193
202
|
def self.stat(path)
|
|
194
|
-
Stat.new(path, true)
|
|
203
|
+
Stat.new(path, dereference: true)
|
|
195
204
|
end
|
|
196
205
|
|
|
197
206
|
def self.symlink(old_name, new_name)
|
|
@@ -200,7 +209,7 @@ module MemFs
|
|
|
200
209
|
end
|
|
201
210
|
|
|
202
211
|
def self.symlink?(path)
|
|
203
|
-
lstat_query(path, :symlink?)
|
|
212
|
+
lstat_query?(path, :symlink?)
|
|
204
213
|
end
|
|
205
214
|
|
|
206
215
|
def self.truncate(path, length)
|
|
@@ -217,12 +226,10 @@ module MemFs
|
|
|
217
226
|
end
|
|
218
227
|
|
|
219
228
|
def self.unlink(*paths)
|
|
220
|
-
paths.each
|
|
221
|
-
fs.unlink(path)
|
|
222
|
-
end
|
|
229
|
+
paths.each { |path| fs.unlink(path) }
|
|
223
230
|
paths.size
|
|
224
231
|
end
|
|
225
|
-
class << self;
|
|
232
|
+
class << self; alias delete unlink; end
|
|
226
233
|
|
|
227
234
|
def self.utime(atime, mtime, *file_names)
|
|
228
235
|
file_names.each do |file_name|
|
|
@@ -232,18 +239,20 @@ module MemFs
|
|
|
232
239
|
file_names.size
|
|
233
240
|
end
|
|
234
241
|
|
|
235
|
-
attr_reader :path
|
|
242
|
+
attr_reader :path # rubocop:disable Lint/DuplicateMethods
|
|
236
243
|
|
|
244
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
237
245
|
def initialize(filename, mode = File::RDONLY, *perm_and_or_opt)
|
|
246
|
+
super()
|
|
247
|
+
|
|
238
248
|
opt = perm_and_or_opt.last.is_a?(Hash) ? perm_and_or_opt.pop : {}
|
|
239
249
|
perm_and_or_opt.shift
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
end
|
|
250
|
+
|
|
251
|
+
fail ArgumentError, 'wrong number of arguments (4 for 1..3)' if perm_and_or_opt.any?
|
|
243
252
|
|
|
244
253
|
@path = filename
|
|
245
|
-
@external_encoding =
|
|
246
|
-
|
|
254
|
+
@external_encoding =
|
|
255
|
+
opt[:external_encoding] && Encoding.find(opt[:external_encoding])
|
|
247
256
|
|
|
248
257
|
self.closed = false
|
|
249
258
|
self.opening_mode = str_to_mode_int(mode)
|
|
@@ -254,16 +263,19 @@ module MemFs
|
|
|
254
263
|
# FIXME: this is an ugly way to ensure a symlink has a target
|
|
255
264
|
entry.dereferenced
|
|
256
265
|
|
|
257
|
-
if entry.respond_to?(:pos=)
|
|
258
|
-
entry.pos = 0
|
|
259
|
-
end
|
|
266
|
+
entry.pos = 0 if entry.respond_to?(:pos=)
|
|
260
267
|
entry.content.clear if truncate_file?
|
|
261
268
|
end
|
|
269
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
262
270
|
|
|
263
271
|
def atime
|
|
264
272
|
File.atime(path)
|
|
265
273
|
end
|
|
266
274
|
|
|
275
|
+
def birthtime
|
|
276
|
+
File.birthtime(path)
|
|
277
|
+
end
|
|
278
|
+
|
|
267
279
|
def chmod(mode_int)
|
|
268
280
|
fs.chmod(mode_int, path)
|
|
269
281
|
SUCCESS
|
|
@@ -300,11 +312,7 @@ module MemFs
|
|
|
300
312
|
|
|
301
313
|
def self.dereference_name(path)
|
|
302
314
|
entry = fs.find(path)
|
|
303
|
-
|
|
304
|
-
entry.dereferenced_name
|
|
305
|
-
else
|
|
306
|
-
basename(path)
|
|
307
|
-
end
|
|
315
|
+
entry ? entry.dereferenced_name : basename(path)
|
|
308
316
|
end
|
|
309
317
|
private_class_method :dereference_name
|
|
310
318
|
|
|
@@ -328,16 +336,16 @@ module MemFs
|
|
|
328
336
|
end
|
|
329
337
|
private_class_method :original_file_class
|
|
330
338
|
|
|
331
|
-
def self.stat_query(path, query, force_boolean
|
|
339
|
+
def self.stat_query(path, query, force_boolean: true)
|
|
332
340
|
response = fs.find(path) && stat(path).public_send(query)
|
|
333
341
|
force_boolean ? !!response : response
|
|
334
342
|
end
|
|
335
343
|
private_class_method :stat_query
|
|
336
344
|
|
|
337
|
-
def self.lstat_query(path, query)
|
|
345
|
+
def self.lstat_query?(path, query)
|
|
338
346
|
response = fs.find(path) && lstat(path).public_send(query)
|
|
339
347
|
!!response
|
|
340
348
|
end
|
|
341
|
-
private_class_method :lstat_query
|
|
349
|
+
private_class_method :lstat_query?
|
|
342
350
|
end
|
|
343
351
|
end
|
data/lib/memfs/file_system.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'singleton'
|
|
2
4
|
require 'memfs/fake/directory'
|
|
3
5
|
require 'memfs/fake/file'
|
|
@@ -7,9 +9,9 @@ module MemFs
|
|
|
7
9
|
class FileSystem
|
|
8
10
|
include Singleton
|
|
9
11
|
|
|
10
|
-
attr_accessor :
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
attr_accessor :registred_entries,
|
|
13
|
+
:root,
|
|
14
|
+
:working_directory
|
|
13
15
|
|
|
14
16
|
def basename(path)
|
|
15
17
|
File.basename(path)
|
|
@@ -27,9 +29,10 @@ module MemFs
|
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def clear!
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
MemFs.reset_platform_root!
|
|
33
|
+
self.root = Fake::Directory.new(MemFs.platform_root)
|
|
34
|
+
mkdir File.join(MemFs.platform_root, 'tmp')
|
|
35
|
+
chdir MemFs.platform_root
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def chmod(mode_int, file_name)
|
|
@@ -51,7 +54,9 @@ module MemFs
|
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def find(path)
|
|
54
|
-
|
|
57
|
+
path = MemFs.normalize_path(path)
|
|
58
|
+
|
|
59
|
+
if MemFs.root_path?(path)
|
|
55
60
|
root
|
|
56
61
|
elsif dirname(path) == '.'
|
|
57
62
|
working_directory.find(path)
|
|
@@ -80,7 +85,7 @@ module MemFs
|
|
|
80
85
|
def getwd
|
|
81
86
|
working_directory.path
|
|
82
87
|
end
|
|
83
|
-
|
|
88
|
+
alias pwd getwd
|
|
84
89
|
|
|
85
90
|
def initialize
|
|
86
91
|
clear!
|
|
@@ -96,7 +101,7 @@ module MemFs
|
|
|
96
101
|
find_parent!(new_name).add_entry link
|
|
97
102
|
end
|
|
98
103
|
|
|
99
|
-
def mkdir(path, mode =
|
|
104
|
+
def mkdir(path, mode = 0o777)
|
|
100
105
|
fail Errno::EEXIST, path if find(path)
|
|
101
106
|
directory = Fake::Directory.new(path)
|
|
102
107
|
directory.mode = mode
|
data/lib/memfs/io.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'forwardable'
|
|
2
4
|
require 'memfs/filesystem_access'
|
|
3
5
|
|
|
@@ -6,23 +8,17 @@ module MemFs
|
|
|
6
8
|
extend SingleForwardable
|
|
7
9
|
include OriginalFile::Constants
|
|
8
10
|
|
|
9
|
-
(OriginalIO.constants - OriginalFile::Constants.constants)
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
end
|
|
11
|
+
(OriginalIO.constants - OriginalFile::Constants.constants).each do |const_name|
|
|
12
|
+
const_set(const_name, OriginalIO.const_get(const_name))
|
|
13
|
+
end
|
|
13
14
|
|
|
14
15
|
def_delegators :original_io_class,
|
|
15
16
|
:copy_stream
|
|
16
17
|
|
|
17
18
|
def self.read(path, *args)
|
|
18
19
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
19
|
-
options = {
|
|
20
|
-
|
|
21
|
-
encoding: nil,
|
|
22
|
-
open_args: nil
|
|
23
|
-
}.merge(options)
|
|
24
|
-
open_args = options[:open_args] ||
|
|
25
|
-
[options[:mode], encoding: options[:encoding]]
|
|
20
|
+
options = { encoding: nil, mode: File::RDONLY, open_args: nil }.merge(options)
|
|
21
|
+
open_args = options[:open_args] || [options[:mode], { encoding: options[:encoding] }]
|
|
26
22
|
|
|
27
23
|
length, offset = args
|
|
28
24
|
|
|
@@ -30,11 +26,12 @@ module MemFs
|
|
|
30
26
|
file.seek(offset || 0)
|
|
31
27
|
file.read(length)
|
|
32
28
|
ensure
|
|
33
|
-
file
|
|
29
|
+
file&.close
|
|
34
30
|
end
|
|
35
31
|
|
|
32
|
+
# rubocop:disable Metrics/MethodLength
|
|
36
33
|
def self.write(path, string, offset = 0, open_args = nil)
|
|
37
|
-
open_args ||= [File::WRONLY, encoding: nil]
|
|
34
|
+
open_args ||= [File::WRONLY, { encoding: nil }]
|
|
38
35
|
|
|
39
36
|
offset = 0 if offset.nil?
|
|
40
37
|
unless offset.respond_to?(:to_int)
|
|
@@ -42,17 +39,17 @@ module MemFs
|
|
|
42
39
|
end
|
|
43
40
|
offset = offset.to_int
|
|
44
41
|
|
|
45
|
-
if offset
|
|
46
|
-
fail
|
|
47
|
-
'MemFs::IO.write with offset not yet supported.'
|
|
42
|
+
if offset.positive?
|
|
43
|
+
fail(NotImplementedError, 'MemFs::IO.write with offset not yet supported.')
|
|
48
44
|
end
|
|
49
45
|
|
|
50
46
|
file = open(path, *open_args)
|
|
51
47
|
file.seek(offset)
|
|
52
48
|
file.write(string)
|
|
53
49
|
ensure
|
|
54
|
-
file
|
|
50
|
+
file&.close
|
|
55
51
|
end
|
|
52
|
+
# rubocop:enable Metrics/MethodLength
|
|
56
53
|
|
|
57
54
|
def self.original_io_class
|
|
58
55
|
MemFs::OriginalIO
|
|
@@ -60,7 +57,7 @@ module MemFs
|
|
|
60
57
|
private_class_method :original_io_class
|
|
61
58
|
|
|
62
59
|
attr_writer :autoclose,
|
|
63
|
-
|
|
60
|
+
:close_on_exec
|
|
64
61
|
|
|
65
62
|
def <<(object)
|
|
66
63
|
fail IOError, 'not opened for writing' unless writable?
|
|
@@ -69,18 +66,11 @@ module MemFs
|
|
|
69
66
|
end
|
|
70
67
|
|
|
71
68
|
def advise(advice_type, _offset = 0, _len = 0)
|
|
72
|
-
advice_types = [
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
:sequential,
|
|
78
|
-
:willneed
|
|
79
|
-
]
|
|
80
|
-
unless advice_types.include?(advice_type)
|
|
81
|
-
fail NotImplementedError, "Unsupported advice: #{advice_type.inspect}"
|
|
82
|
-
end
|
|
83
|
-
nil
|
|
69
|
+
advice_types = %i[dontneed noreuse normal random sequential willneed]
|
|
70
|
+
|
|
71
|
+
return if advice_types.include?(advice_type)
|
|
72
|
+
|
|
73
|
+
fail NotImplementedError, "Unsupported advice: #{advice_type.inspect}"
|
|
84
74
|
end
|
|
85
75
|
|
|
86
76
|
def autoclose?
|
|
@@ -112,7 +102,7 @@ module MemFs
|
|
|
112
102
|
def eof?
|
|
113
103
|
pos >= content.size
|
|
114
104
|
end
|
|
115
|
-
|
|
105
|
+
alias eof eof?
|
|
116
106
|
|
|
117
107
|
def external_encoding
|
|
118
108
|
if writable?
|
|
@@ -122,28 +112,32 @@ module MemFs
|
|
|
122
112
|
end
|
|
123
113
|
end
|
|
124
114
|
|
|
125
|
-
def each(sep =
|
|
126
|
-
return to_enum(__callee__) unless block_given?
|
|
115
|
+
def each(sep = $/, &block)
|
|
116
|
+
return to_enum(__callee__, sep) unless block_given?
|
|
127
117
|
fail IOError, 'not opened for reading' unless readable?
|
|
128
|
-
content.each_line(sep
|
|
118
|
+
content.each_line(sep, &block)
|
|
129
119
|
self
|
|
130
120
|
end
|
|
131
121
|
|
|
132
|
-
def each_byte
|
|
122
|
+
def each_byte(&block)
|
|
133
123
|
return to_enum(__callee__) unless block_given?
|
|
134
124
|
fail IOError, 'not opened for reading' unless readable?
|
|
135
|
-
content.each_byte
|
|
125
|
+
content.each_byte(&block)
|
|
136
126
|
self
|
|
137
127
|
end
|
|
138
|
-
|
|
128
|
+
alias bytes each_byte
|
|
139
129
|
|
|
140
|
-
def each_char
|
|
130
|
+
def each_char(&block)
|
|
141
131
|
return to_enum(__callee__) unless block_given?
|
|
142
132
|
fail IOError, 'not opened for reading' unless readable?
|
|
143
|
-
content.each_char
|
|
133
|
+
content.each_char(&block)
|
|
144
134
|
self
|
|
145
135
|
end
|
|
146
|
-
|
|
136
|
+
alias chars each_char
|
|
137
|
+
|
|
138
|
+
def fileno
|
|
139
|
+
entry.fileno
|
|
140
|
+
end
|
|
147
141
|
|
|
148
142
|
def pos
|
|
149
143
|
entry.pos
|
|
@@ -165,22 +159,22 @@ module MemFs
|
|
|
165
159
|
content.puts text
|
|
166
160
|
end
|
|
167
161
|
|
|
168
|
-
def read(length = nil, buffer = '')
|
|
169
|
-
unless entry
|
|
170
|
-
|
|
171
|
-
end
|
|
162
|
+
def read(length = nil, buffer = +'')
|
|
163
|
+
fail(Errno::ENOENT, path) unless entry
|
|
164
|
+
|
|
172
165
|
default = length ? nil : ''
|
|
173
166
|
content.read(length, buffer) || default
|
|
174
167
|
end
|
|
175
168
|
|
|
176
169
|
def seek(amount, whence = ::IO::SEEK_SET)
|
|
177
|
-
new_pos =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
170
|
+
new_pos =
|
|
171
|
+
case whence
|
|
172
|
+
when ::IO::SEEK_CUR then entry.pos + amount
|
|
173
|
+
when ::IO::SEEK_END then content.to_s.length + amount
|
|
174
|
+
when ::IO::SEEK_SET then amount
|
|
175
|
+
end
|
|
182
176
|
|
|
183
|
-
fail Errno::EINVAL, path if new_pos.nil? || new_pos
|
|
177
|
+
fail Errno::EINVAL, path if new_pos.nil? || new_pos.negative?
|
|
184
178
|
|
|
185
179
|
entry.pos = new_pos
|
|
186
180
|
0
|
|
@@ -191,7 +185,7 @@ module MemFs
|
|
|
191
185
|
end
|
|
192
186
|
|
|
193
187
|
def write(string)
|
|
194
|
-
fail
|
|
188
|
+
fail(IOError, 'not opened for writing') unless writable?
|
|
195
189
|
|
|
196
190
|
content.write(string.to_s)
|
|
197
191
|
end
|
|
@@ -199,8 +193,8 @@ module MemFs
|
|
|
199
193
|
private
|
|
200
194
|
|
|
201
195
|
attr_accessor :closed,
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
:entry,
|
|
197
|
+
:opening_mode
|
|
204
198
|
|
|
205
199
|
attr_reader :path
|
|
206
200
|
|
|
@@ -220,7 +214,7 @@ module MemFs
|
|
|
220
214
|
def str_to_mode_int(mode)
|
|
221
215
|
return mode unless mode.is_a?(String)
|
|
222
216
|
|
|
223
|
-
unless mode =~ /\A([rwa]\+?)([bt])?(:bom)?(\|.+)?\z/
|
|
217
|
+
unless mode =~ /\A([rwa]\+?)([bt])?(:(bom|UTF-8|utf-8))?(\|.+)?\z/
|
|
224
218
|
fail ArgumentError, "invalid access mode #{mode}"
|
|
225
219
|
end
|
|
226
220
|
|
data/lib/memfs/version.rb
CHANGED
data/lib/memfs.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'memfs/version'
|
|
2
4
|
require 'fileutils'
|
|
3
5
|
|
|
@@ -25,6 +27,86 @@ module MemFs
|
|
|
25
27
|
# Keeps track of the original Ruby IO class.
|
|
26
28
|
OriginalIO = ::IO
|
|
27
29
|
|
|
30
|
+
def self.ruby_version_gte?(version) # :nodoc:
|
|
31
|
+
Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.windows?
|
|
35
|
+
/mswin|bccwin|mingw/ =~ RUBY_PLATFORM
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the platform-specific root path (e.g., '/' on Unix, 'D:/' on Windows)
|
|
39
|
+
def self.platform_root
|
|
40
|
+
@platform_root || default_platform_root
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Allows setting a custom platform root (mainly for testing)
|
|
44
|
+
def self.platform_root=(value)
|
|
45
|
+
@platform_root = value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Resets platform_root to the default value
|
|
49
|
+
def self.reset_platform_root!
|
|
50
|
+
@platform_root = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the default platform root based on the current OS
|
|
54
|
+
def self.default_platform_root
|
|
55
|
+
if windows?
|
|
56
|
+
# Normalize drive letter to uppercase
|
|
57
|
+
OriginalFile.expand_path('/').sub(/\A([a-z]):/) { "#{::Regexp.last_match(1).upcase}:" }
|
|
58
|
+
else
|
|
59
|
+
'/'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check if a path is the root path (handles both '/' and 'D:/')
|
|
64
|
+
def self.root_path?(path)
|
|
65
|
+
return false if path.nil?
|
|
66
|
+
|
|
67
|
+
normalized = normalize_path(path)
|
|
68
|
+
normalized == platform_root || normalized == '/'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Normalize path for consistent handling
|
|
72
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
73
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
74
|
+
def self.normalize_path(path)
|
|
75
|
+
return path unless path.is_a?(String)
|
|
76
|
+
|
|
77
|
+
# Reject UNC paths
|
|
78
|
+
fail ArgumentError, "UNC paths are not supported: #{path}" if path.start_with?('\\\\', '//')
|
|
79
|
+
|
|
80
|
+
# Convert backslashes to forward slashes
|
|
81
|
+
path = path.tr('\\', '/')
|
|
82
|
+
|
|
83
|
+
return path unless windows?
|
|
84
|
+
|
|
85
|
+
# Normalize drive letter to uppercase
|
|
86
|
+
path = path.sub(/\A([a-z]):/) { "#{::Regexp.last_match(1).upcase}:" }
|
|
87
|
+
|
|
88
|
+
# Handle drive-relative paths like 'D:foo' or 'D:.' (no slash after colon)
|
|
89
|
+
# and bare drive letters like 'D:' (current directory on drive D)
|
|
90
|
+
# Convert to absolute paths since our fake fs doesn't support per-drive working directories
|
|
91
|
+
if path.match?(/\A[A-Z]:\z/) # Bare drive like 'D:'
|
|
92
|
+
path = "#{path}/"
|
|
93
|
+
elsif path.match?(%r{\A[A-Z]:[^/]}) # Drive-relative like 'D:foo' or 'D:.'
|
|
94
|
+
path = path.sub(/\A([A-Z]):/, '\1:/')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Convert bare '/' to platform root on Windows
|
|
98
|
+
if path == '/'
|
|
99
|
+
platform_root
|
|
100
|
+
elsif path.start_with?('/') && !path.match?(%r{\A[A-Z]:/})
|
|
101
|
+
# Convert '/foo' to 'D:/foo' on Windows
|
|
102
|
+
"#{platform_root}#{path[1..]}"
|
|
103
|
+
else
|
|
104
|
+
path
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
108
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
109
|
+
|
|
28
110
|
require 'memfs/file_system'
|
|
29
111
|
require 'memfs/dir'
|
|
30
112
|
require 'memfs/file'
|
|
@@ -133,9 +215,7 @@ module MemFs
|
|
|
133
215
|
#
|
|
134
216
|
# @return nothing.
|
|
135
217
|
def touch(*paths)
|
|
136
|
-
if ::File != MemFs::File
|
|
137
|
-
fail 'Always call MemFs.touch inside a MemFs active context.'
|
|
138
|
-
end
|
|
218
|
+
fail 'Always call MemFs.touch inside a MemFs active context.' if ::File != MemFs::File
|
|
139
219
|
|
|
140
220
|
paths.each do |path|
|
|
141
221
|
FileUtils.mkdir_p File.dirname(path)
|
data/memfs.gemspec
CHANGED
|
@@ -12,23 +12,6 @@ Gem::Specification.new do |gem|
|
|
|
12
12
|
'for tests. Strongly inspired by FakeFS.'
|
|
13
13
|
gem.summary = "memfs-#{MemFs::VERSION}"
|
|
14
14
|
gem.homepage = 'http://github.com/simonc/memfs'
|
|
15
|
-
|
|
16
15
|
gem.license = 'MIT'
|
|
17
|
-
|
|
18
16
|
gem.files = `git ls-files`.split($/)
|
|
19
|
-
gem.executables = gem.files.grep(/^bin\//).map { |f| File.basename(f) }
|
|
20
|
-
gem.test_files = gem.files.grep(/^(test|spec|features)\//)
|
|
21
|
-
gem.require_paths = ['lib']
|
|
22
|
-
|
|
23
|
-
gem.add_development_dependency 'coveralls', '~> 0.6'
|
|
24
|
-
gem.add_development_dependency 'rake', '~> 12.0'
|
|
25
|
-
gem.add_development_dependency 'rspec', '~> 3.0'
|
|
26
|
-
gem.add_development_dependency 'guard', '~> 2.6'
|
|
27
|
-
gem.add_development_dependency 'guard-rspec', '~> 4.3'
|
|
28
|
-
gem.add_development_dependency 'rb-inotify', '~> 0.8'
|
|
29
|
-
gem.add_development_dependency 'rb-fsevent', '~> 0.9'
|
|
30
|
-
gem.add_development_dependency 'rb-fchange', '~> 0.0'
|
|
31
|
-
|
|
32
|
-
listen_version = RUBY_VERSION >= '2.2.3' ? '~> 3.1' : '~> 3.0.7'
|
|
33
|
-
gem.add_development_dependency 'listen', listen_version
|
|
34
17
|
end
|