require 'stringio' module FakeFS # FakeFS File class inherit StringIO class File < StringIO MODES = [ READ_ONLY = 'r'.freeze, READ_WRITE = 'r+'.freeze, WRITE_ONLY = 'w'.freeze, READ_WRITE_TRUNCATE = 'w+'.freeze, APPEND_WRITE_ONLY = 'a'.freeze, APPEND_READ_WRITE = 'a+'.freeze ].freeze FMODE_READABLE = 0x00000001 FMODE_WRITABLE = 0x00000002 FMODE_READWRITE = (FMODE_READABLE | FMODE_WRITABLE) FMODE_BINMODE = 0x00000004 FMODE_APPEND = 0x00000040 FMODE_CREATE = 0x00000080 FMODE_EXCL = 0x00000400 FMODE_TRUNC = 0x00000800 FMODE_TEXTMODE = 0x00001000 TEXT_MODES = { 'r' => FMODE_READABLE, 'w' => FMODE_WRITABLE | FMODE_TRUNC | FMODE_CREATE, 'a' => FMODE_WRITABLE | FMODE_APPEND | FMODE_CREATE }.freeze FILE_CREATION_MODES = (MODES - [READ_ONLY, READ_WRITE]).freeze FILE_ACCESS_MODE = (RealFile::RDONLY | RealFile::WRONLY | RealFile::RDWR) # copied from https://github.com/ruby/ruby/blob/v2_7_8/include/ruby/io.h#L108 MODE_BITMASK = ( RealFile::RDONLY | RealFile::WRONLY | RealFile::RDWR | RealFile::APPEND | RealFile::CREAT | RealFile::EXCL | RealFile::NONBLOCK | RealFile::TRUNC | RealFile::BINARY | (RealFile.const_defined?(:NOCTTY) ? RealFile::NOCTTY : 0) | (RealFile.const_defined?(:SYNC) ? RealFile::SYNC : 0) ) def self.extname(path) RealFile.extname(path) end def self.join(*parts) RealFile.join(parts) end def self.path(file) RealFile.path(file) end def self.exist?(path) if File.symlink?(path) referent = File.expand_path(File.readlink(path), File.dirname(path)) exist?(referent) else !FileSystem.find(path).nil? end end class << self def identical?(one_path, another_path) FileSystem.find(one_path) == FileSystem.find(another_path) end # Assume nothing is sticky. def sticky?(_path) false end end def self.readable?(path) return false unless exist? path File.lstat(path).readable? end def self.writable?(path) return false unless exist? path File.lstat(path).writable? end def self.mtime(path) if exist?(path) FileSystem.find(path).mtime else raise Errno::ENOENT end end def self.ctime(path) if exist?(path) FileSystem.find(path).ctime else raise Errno::ENOENT end end def self.atime(path) if exist?(path) FileSystem.find(path).atime else raise Errno::ENOENT end end def self.utime(atime, mtime, *paths) paths.each do |path| if exist?(path) FileSystem.find(path).atime = atime FileSystem.find(path).mtime = mtime else raise Errno::ENOENT end end paths.size end def self.size(path) if directory?(path) 64 + (32 * FileSystem.find(path).entries.size) else read(path).bytesize end end def self.size?(path) size(path) if exist?(path) && !size(path).zero? end def self.zero?(path) exist?(path) && size(path) == 0 end if RUBY_VERSION >= '2.4' class << self alias empty? zero? end end def self.const_missing(name) RealFile.const_get(name) end def self.directory?(path) if path.respond_to? :entry path.entry.is_a? FakeDir else result = FileSystem.find(path) result ? result.entry.is_a?(FakeDir) : false end end def self.symlink?(path) if path.respond_to? :entry path.is_a? FakeSymlink else FileSystem.find(path).is_a? FakeSymlink end end def self.file?(path) if path.respond_to? :entry path.entry.is_a? FakeFile else result = FileSystem.find(path) result ? result.entry.is_a?(FakeFile) : false end end def self.ftype(filename) File.lstat(filename).ftype end def self.expand_path(file_name, dir_string = FileSystem.current_dir.to_s) RealFile.expand_path(file_name, RealFile.expand_path(dir_string, Dir.pwd)) end def self.basename(*args) RealFile.basename(*args) end def self.dirname(*args) RealFile.dirname(*args) end def self.readlink(path) symlink = FileSystem.find(path) symlink.target end # TODO: support open_key_args def self.read(path, *args) options = args[-1].is_a?(Hash) ? args.pop : {} length = args.empty? ? nil : args.shift offset = args.empty? ? 0 : args.shift file = new(path, **options) raise Errno::ENOENT unless file.exists? raise Errno::EISDIR, path.to_s if directory?(path) FileSystem.find(path).atime = Time.now file.seek(offset) file.read(length) end def self.readlines(path, chomp: false) file = new(path) if file.exists? FileSystem.find(path).atime = Time.now chomp ? file.readlines.map(&:chomp) : file.readlines else raise Errno::ENOENT end end def self.foreach(path, *args, &block) file = new(path) if file.exists? FileSystem.find(path).atime = Time.now if block_given? file.each_line(*args, &block) else file.each_line(*args) end else raise Errno::ENOENT end end def self.rename(source, dest) if directory?(source) && file?(dest) raise Errno::ENOTDIR, "#{source} or #{dest}" elsif file?(source) && directory?(dest) raise Errno::EISDIR, "#{source} or #{dest}" elsif !exist?(dirname(dest)) raise Errno::ENOENT, "#{source} or #{dest}" end if (target = FileSystem.find(source)) return 0 if source == dest if target.is_a?(FakeFS::FakeSymlink) File.symlink(target.target, dest) else FileSystem.add(dest, target.entry.clone) end FileSystem.delete(source) else raise Errno::ENOENT, "#{source} or #{dest}" end 0 end def self.link(source, dest) raise Errno::EPERM, "#{source} or #{dest}" if directory?(source) raise Errno::ENOENT, "#{source} or #{dest}" unless exist?(source) raise Errno::EEXIST, "#{source} or #{dest}" if exist?(dest) source = FileSystem.find(source) dest = FileSystem.add(dest, source.entry.clone) source.link(dest) 0 end def self.delete(*files) files.each do |file| file_name = (file.class == FakeFS::File ? file.path : file.to_s) raise Errno::ENOENT, file_name unless exist?(file_name) FileUtils.rm(file_name) end files.size end class << self alias unlink delete end def self.symlink(source, dest) FileUtils.ln_s(source, dest) end def self.stat(file) File::Stat.new(file) end def self.lstat(file) File::Stat.new(file, true) end def self.split(path) RealFile.split(path) end def self.chmod(new_mode, filename) # chmod's mode can either be passed in in absolute mode, or symbolic mode # for reference: https://ruby-doc.org/stdlib-2.2.2/libdoc/fileutils/rdoc/FileUtils.html#method-c-chmod # if the mode is passed in symbolic mode we must convert it to absolute mode is_absolute_mode = new_mode.is_a? Numeric unless is_absolute_mode current_mode = FileSystem.find(filename).mode new_mode = convert_symbolic_chmod_to_absolute(new_mode, current_mode) end FileSystem.find(filename).mode = 0o100000 + new_mode end # Not exactly right, returns true if the file is chmod +x for owner. In the # context of when you would use fakefs, this is usually what you want. def self.executable?(filename) file = FileSystem.find(filename) return false unless file (file.mode - 0o100000) & 0o100 != 0 end def self.chown(owner_int, group_int, filename) file = FileSystem.find(filename) if owner_int && owner_int != -1 owner_int.is_a?(Integer) || raise(TypeError, "can't convert String into Integer") file.uid = owner_int end if group_int && group_int != -1 group_int.is_a?(Integer) || raise(TypeError, "can't convert String into Integer") file.gid = group_int end end def self.umask(*args) RealFile.umask(*args) end def self.binread(file, length = nil, offset = 0) File.read(file, length, offset, mode: 'rb:ASCII-8BIT') end def self.binwrite(file, content, offset = nil) mode = offset ? 'r+b:ASCII-8BIT' : 'wb:ASCII-8BIT' File.write(file, content, offset, mode: mode) end def self.fnmatch?(pattern, path, flags = 0) RealFile.fnmatch?(pattern, path, flags) end class << self alias fnmatch fnmatch? end # FakeFS Stat class class Stat attr_reader :ctime, :mtime, :atime, :mode, :uid, :gid attr_reader :birthtime def initialize(file, lstat = false) raise(Errno::ENOENT, file.to_s) unless File.exist?(file) @file = file @fake_file = FileSystem.find(@file) @__lstat = lstat @ctime = @fake_file.ctime @mtime = @fake_file.mtime @atime = @fake_file.atime @mode = @fake_file.mode @uid = @fake_file.uid @gid = @fake_file.gid @inode = @fake_file.inode @birthtime = if @fake_file.respond_to?(:birthtime) @fake_file.birthtime else @fake_file.ctime end end def symlink? File.symlink?(@file) end def directory? File.directory?(@file) end def file? File.file?(@file) end def ftype return 'link' if symlink? return 'directory' if directory? 'file' end def readable? # a file is readable if, and only if, it has the following bits: # 4 ( read permission ) # 5 ( read + execute permission ) # 6 ( read + write permission ) # 7 ( read + write + execute permission ) # for each group we will isolate the wanted numbers ( for owner, world, or group ) # and see if the third bit is set ( as that is the bit for read ) read_bit = 4 check_if_bit_set(read_bit) end def writable? # a file is writable if, and only if, it has the following bits: # 2 ( write permission ) # 3 ( write + execute permission ) # 6 ( read + write permission ) # 7 ( read + write + execute permission ) # for each group we will isolate the wanted numbers ( for owner, world, or group ) # and see if the second bit is set ( as that is the bit for write ) write_bit = 2 check_if_bit_set(write_bit) end # Assume nothing is sticky. def sticky? false end # World_writable and readable are platform dependent # usually comparing with S_IROTH defined on compilation (MRI) def world_writable? 0o777 end def world_readable? 0o777 end def nlink @fake_file.links.size end def size if @__lstat && symlink? @fake_file.target.size else File.size(@file) end end def zero? size == 0 end def ino @inode.inode_num end include Comparable def <=>(other) @mtime <=> other.mtime end private def check_if_bit_set(bit) # get user's group and user ids # NOTE: I am picking `Process` over `Etc` as we use `Process` # when instaniating file classes. It may be worth it to ensure # our Process/Group detection scheme is robust in all cases uid = Process.uid gid = Process.gid # check if bit set for owner owner_bits = (@mode >> 6) & 0o7 if uid == @uid # the user is locked out of the file if they are owner of the file # but do not have the bit set at the user level return true if owner_bits & bit == bit return false end # check if bit set for group group_bits = (@mode >> 3) & 0o7 if gid == @gid # the user is locked out of the file if they are in the group that # owns the file but do not have the bit set at the group level return true if group_bits & bit == bit return false end # check if bit set for world world_bits = @mode & 0o7 return true if world_bits & bit == bit false end end attr_reader :path def initialize(path, *args) # unable to pass args otherwise on jruby, it may cause false passes on MRI, though # because explicit hash isn't supported opts = args.last.is_a?(Hash) ? args.pop : {} if args.size > 2 raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 1..3)" end mode, _perm = args @path = path @file = FileSystem.find(@path) # real rb_scan_open_args - and rb_io_extract_modeenc - is much more complex raise ArgumentError, 'mode specified twice' unless mode.nil? || opts[:mode].nil? mode_opt = mode.nil? ? opts[:mode] : mode # see vmode_handle if mode_opt.nil? @oflags = RealFile::RDONLY elsif mode_opt.respond_to?(:to_int) && (intmode = mode_opt.to_int).instance_of?(Integer) @oflags = intmode else unless mode_opt.is_a?(String) unless mode_opt.respond_to?(:to_str) raise TypeError, "no implicit conversion of #{mode_opt.class} into String" end strmode = mode_opt.to_str unless strmode.is_a?(String) raise TypeError, "can't convert #{mode_opt.class} to String " \ "(#{mode_opt.class}#to_str gives #{strmode.class})" end mode_opt = strmode end @oflags, @fmode = parse_strmode_oflags(mode_opt) end unless opts[:flags].nil? if opts[:flags].is_a?(Integer) @oflags |= opts[:flags] elsif opts[:flags].respond_to?(:to_int) intflags = opts[:flags].to_int unless intflags.instance_of?(Integer) raise TypeError, "can't convert #{opts[:flags].class} to Integer " \ "(#{opts[:flags].class}#to_int gives #{intflags.class})" end @oflags |= intflags @fmode = create_fmode(@oflags) else raise TypeError, "no implicit conversion of #{opts[:flags].class} into Integer" end end @fmode ||= create_fmode(@oflags) @fmode = extract_binmode(opts, @fmode) @autoclose = true file_creation_mode? ? create_missing_file : check_file_existence! # StringIO changes enciding of the underlying string to binary # when binary data is written if it's opened in binary mode, # so content might have binary encoding. StringIO also switches to # binary mode if its string have binary encoding, but it might not # be what we want, so insteed we use encoding parsed after super call # and force set it back. # truncate doesn't work @file.content.force_encoding(Encoding.default_external) # StringIO.new 'content', nil, **{} # works in MRI, but fails in JRuby # but File.open 'filename', nil, **{} is ok both in MRI and JRuby # JRuby StringIO doesn't support kwargs without mode # StringIO.new "buff", encoding: 'binary' # work on MRI, fails on JRuby if RUBY_PLATFORM == "java" # other opts aren't supported super(@file.content, mode_opt || 'r') binmode if binmode? # Looks like it doesn't care about 'b' mode_opt_str = mode_opt.is_a?(String) ? mode_opt : '' raise ArgumentError, 'encoding specified twice' if mode_opt_str[':'] && opts[:encoding] # Might raise where real File just warns str_encoding = mode_opt_str.split(':')[1] # internal encoding is ignored anyway if opts[:encoding] set_encoding(opts[:encoding]) elsif str_encoding && str_encoding != '' set_encoding(str_encoding) elsif opts[:binmode] set_encoding(Encoding::BINARY) end else super(@file.content, mode, **opts) end # StringIO is wrtable and readable by default, so we need to disable it # but maybe it was explicitly disabled by opts close_write if @fmode & FMODE_WRITABLE == 0 && !StringIO.instance_method(:closed_write?).bind(self).call close_read if @fmode & FMODE_READABLE == 0 && !StringIO.instance_method(:closed_read?).bind(self).call end def exists? true end def write(*args) val = super(*args) @file.mtime = Time.now val end alias tell= pos= alias sysread read alias syswrite write undef_method :closed_read? undef_method :closed_write? undef_method :length undef_method :size undef_method :string undef_method :string= if RUBY_PLATFORM == 'java' undef_method :to_channel undef_method :to_outputstream undef_method :to_inputstream # JRuby 9.2.0.0 undef_method :to_output_stream if respond_to?(:to_output_stream) undef_method :to_input_stream if respond_to?(:to_input_stream) end def is_a?(klass) RealFile.allocate.is_a?(klass) end def string gets(nil) end def ioctl(*) raise NotImplementedError end def read_nonblock raise NotImplementedError end def stat self.class.stat(@path) end def lstat self.class.lstat(@path) end def sysseek(position, whence = SEEK_SET) seek(position, whence) pos end alias to_i fileno def to_io self end def write_nonblock(*) raise NotImplementedError end def readpartial(*) super end def atime self.class.atime(@path) end def ctime self.class.ctime(@path) end def flock(*) raise NotImplementedError end def mtime self.class.mtime(@path) end def chmod(new_mode) # chmod's mode can either be passed in in absolute mode, or symbolic mode # for reference: https://ruby-doc.org/stdlib-2.2.2/libdoc/fileutils/rdoc/FileUtils.html#method-c-chmod # if the mode is passed in symbolic mode we must convert it to absolute mode is_absolute_mode = new_mode.is_a? Numeric unless is_absolute_mode current_mode = @file.mode new_mode = convert_symbolic_chmod_to_absolute(new_mode, current_mode) end @file.mode = 0o100000 + new_mode end def chown(owner_int, group_int) return unless group_int && group_int != -1 owner_int.is_a?(Integer) || raise( TypeError, "can't convert String into Integer" ) @file.uid = owner_int group_int.is_a?(Integer) || raise( TypeError, "can't convert String into Integer" ) @file.gid = group_int end def self.realpath(*args) RealFile.realpath(*args) end def binmode? # File.open('test_mode', mode: 'w:binary').binmode? # => false @fmode & FMODE_BINMODE != 0 end def close_on_exec=(_bool) raise NotImplementedError end def close_on_exec? raise NotImplementedError end def to_path @path end def self.absolute_path(file_name, dir_name = Dir.getwd) RealFile.absolute_path(file_name, dir_name) end attr_accessor :autoclose def autoclose? @autoclose ? true : false end alias fdatasync flush def size File.size(@path) end def self.realdirpath(*args) RealFile.realdirpath(*args) end def advise(_advice, _offset = 0, _len = 0); end def self.write(filename, contents, offset = nil, **open_args) mode = offset ? 'r+' : 'w' if open_args[:open_args] # see open_key_args # todo: foreach, readlines, read also use it # Treat a final argument as keywords if it is a hash, and not as keywords otherwise. open_args = open_args[:open_args] if open_args.last.is_a?(Hash) args = open_args[0...-1] opt = open_args.last else args = open_args opt = {} end else args = [open_args.delete(:mode) || mode] opt = open_args end if offset open(filename, *args, **opt) do |f| # rubocop:disable Security/Open f.seek(offset) f.write(contents) end else open(filename, *args, **opt) do |f| # rubocop:disable Security/Open f.write(contents) end end end def self.birthtime(path) if exist?(path) FileSystem.find(path).birthtime else raise Errno::ENOENT end end def birthtime self.class.birthtime(@path) end def self.convert_symbolic_chmod_to_absolute(new_mode, current_mode) # mode always must be of form =,= 0o100, 'g' => 0o10, 'o' => 0o1 } # make sure we preload the current group values. # chmod works by calculating new permissions based off of existing permissions current_groups_to_vals = { 0o100 => 0o0, 0o10 => 0o0, 0o1 => 0o0 } [0o100, 0o10, 0o1].each do |group_num| perm_amt = get_perms_for_group(current_mode, group_num) current_groups_to_vals[group_num] = perm_amt end chmod_pairs.each do |pair| # see if we are dealing with +/- ( granting or removing permissions ) or = ( assigning permissions ) # note that it IS valid to mix assignment and granting/revoking perissions ( things like u=wrx,g+x are valid ) assign_perms = '=' remove_perms = '-' add_perms = '+' assignment_mode = nil if pair.include? remove_perms assignment_mode = remove_perms elsif pair.include? add_perms assignment_mode = add_perms elsif pair.include? assign_perms assignment_mode = assign_perms end # if we can't find a mode, then raise an exception as real `chmod` would if assignment_mode.nil? raise ArgumentError, "Invalid file mode: #{mode}" end adding_removing_perms = [add_perms, remove_perms].include?(assignment_mode) groups = pair.rpartition(assignment_mode).first modes = pair.rpartition(assignment_mode).last # get the numeric chmod value associated with the symbolic entry chmod_perm_num = calculate_chmod_amt_for_mode modes # if we give no groups, then we are giving all groups if groups == '' if adding_removing_perms [0o100, 0o10, 0o1].each do |group_num| perm_amt = set_perms_for_group(current_groups_to_vals, group_num, assignment_mode, chmod_perm_num) current_groups_to_vals[group_num] = perm_amt end else [0o100, 0o10, 0o1].each do |group_num| current_groups_to_vals[group_num] = chmod_perm_num end end else # make sure there are no invalid flags in the groups and that we discard duplicates as chmod does given_groups = groups.split('') given_groups = given_groups.uniq given_groups.each do |specific_group| # ensure that the group is valid unless valid_groups_to_numeric_vals.key? specific_group raise ArgumentError, "Invalid `who' symbol in file mode: #{specific_group}" end # take the current chmod amt from earlier and associate that as the current chmod factor for the group # if we are adding or removing groups ( via +/- ) then we must make sure that we adjust # the current chmod perm number for the group group_num = valid_groups_to_numeric_vals[specific_group] adjusted_chmod = chmod_perm_num if adding_removing_perms adjusted_chmod = set_perms_for_group(current_groups_to_vals, group_num, assignment_mode, chmod_perm_num) end current_groups_to_vals[group_num] = adjusted_chmod end end end # return an octal chmod value for the value 0o100 * current_groups_to_vals[0o100] + 0o10 * current_groups_to_vals[0o10] + current_groups_to_vals[0o1] end # return the group mode for group num based off the provided current_file_mode def self.get_perms_for_group(current_file_mode, group_num) # get the current recorded mode of the group and return it to the caller # note we don't shift for 'o' since that is the bottom 3 bits # note we multiply by 7 since the group num is 1, and octal represents digits 1-7 and we want all 3 bits current_group_mode = current_file_mode & (group_num * 7) if group_num == 0o100 current_group_mode = current_group_mode >> 6 elsif group_num == 0o10 current_group_mode = current_group_mode >> 3 end current_group_mode end # given the current chmod values for a file return the result of adding or removing chmod_perm_num from the # requested groups permissions ( so performing + or - def self.set_perms_for_group(current_groups_to_vals, group_num, assignment_mode, chmod_perm_num) # get the current recorded mode of the group current_group_mode = current_groups_to_vals[group_num] # now that we have the current value of the group, add or remove bits accordingly if assignment_mode == '+' current_group_mode | chmod_perm_num elsif assignment_mode == '-' current_group_mode & ~chmod_perm_num else raise ArgumentError, "Unknown assignment mode #{assignment_mode}" end end # given a list of modes [rwx] (a) ensure all modes are valid and (b) return the numeric value # associated with the modes def self.calculate_chmod_amt_for_mode(modes) valid_modes_to_numeric_vals = { 'r' => 0o4, 'w' => 0o2, 'x' => 0o1 } # if we give no modes, then we are removing all permission chmod_perm_num = 0o0 if modes != '' # make sure there are no invalid flags in the modes and that we discard duplicates as chmod does given_modes = modes.split('') given_modes = given_modes.uniq given_modes.each do |specific_mode| # ensure that the mode is valid unless valid_modes_to_numeric_vals.key? specific_mode raise ArgumentError, "Invalid `perm' symbol in file mode: #{specific_mode}" end chmod_perm_num += valid_modes_to_numeric_vals[specific_mode] end end chmod_perm_num end # split the private class method decleration so rubocop doesn't complain the line is too long private_class_method :convert_symbolic_chmod_to_absolute, :calculate_chmod_amt_for_mode private_class_method :get_perms_for_group, :set_perms_for_group private def extract_binmode(opts, fmode) textmode = opts[:textmode] unless textmode.nil? raise ArgumentError, "textmode specified twice" if fmode & FMODE_TEXTMODE != 0 raise ArgumentError, "both textmode and binmode specified" if fmode & FMODE_BINMODE != 0 # yep, false has no effect here, but still causes ArgumentError fmode |= FMODE_TEXTMODE if textmode end binmode = opts[:binmode] unless binmode.nil? raise ArgumentError, "binmode specified twice" if fmode & FMODE_BINMODE != 0 raise ArgumentError, "both textmode and binmode specified" if fmode & FMODE_TEXTMODE != 0 fmode |= FMODE_BINMODE if binmode end if fmode & FMODE_BINMODE != 0 && fmode & FMODE_TEXTMODE != 0 raise ArgumentError, "both textmode and binmode specified" end fmode end def create_fmode(oflags) # rb_io_oflags_fmode fmode = 0 case oflags & FILE_ACCESS_MODE when RealFile::RDONLY fmode = FMODE_READABLE when RealFile::WRONLY fmode = FMODE_WRITABLE when RealFile::RDWR fmode = FMODE_READWRITE end fmode |= FMODE_APPEND if (oflags & RealFile::APPEND) != 0 fmode |= FMODE_TRUNC if (oflags & RealFile::TRUNC) != 0 fmode |= FMODE_CREATE if (oflags & RealFile::CREAT) != 0 fmode |= FMODE_EXCL if (oflags & RealFile::EXCL) != 0 fmode |= FMODE_BINMODE if (oflags & RealFile::BINARY) != 0 fmode end def parse_strmode_oflags(mode) # rb_io_modestr_fmode access_mode = mode[0] mode_modificators = mode[1..-1].split(':', 2).first fmode = TEXT_MODES[access_mode] raise ArgumentError, "invalid access mode #{mode}" unless fmode mode_modificators&.each_char do |m| case m when 'b' fmode |= FMODE_BINMODE when 't' fmode |= FMODE_TEXTMODE when '+' fmode |= FMODE_READWRITE when 'x' raise ArgumentError, "invalid access mode #{mode}" unless access_mode == 'w' fmode |= FMODE_EXCL else raise ArgumentError, "invalid access mode #{mode}" end end if (fmode & FMODE_BINMODE).nonzero? && (fmode & FMODE_TEXTMODE).nonzero? raise ArgumentError, "invalid access mode #{mode}" end # rb_io_fmode_oflags oflags = 0 case fmode & FMODE_READWRITE when FMODE_READABLE oflags |= RealFile::RDONLY when FMODE_WRITABLE oflags |= RealFile::WRONLY when FMODE_READWRITE oflags |= RealFile::RDWR end oflags |= RealFile::APPEND if (fmode & FMODE_APPEND).nonzero? oflags |= RealFile::TRUNC if (fmode & FMODE_TRUNC).nonzero? oflags |= RealFile::CREAT if (fmode & FMODE_CREATE).nonzero? oflags |= RealFile::EXCL if (fmode & FMODE_EXCL).nonzero? oflags |= RealFile::BINARY if (fmode & FMODE_BINMODE).nonzero? [oflags, fmode] end def check_file_existence! raise Errno::ENOENT, @path.to_s unless @file end def file_creation_mode? mode_in?(RealFile::CREAT) end def mode_in?(mask) (@oflags & mask) != 0 end # Create a missing file if the path is valid. # def create_missing_file if @file raise Errno::EEXIST, @path.to_s if mode_in?(RealFile::EXCL) raise Errno::EISDIR, path.to_s if File.directory?(@path) return end dirname = RealFile.dirname @path unless dirname == '.' dir = FileSystem.find dirname raise Errno::ENOENT, path.to_s unless dir.is_a? FakeDir end @file = FileSystem.add(path, FakeFile.new) end end end