summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authoraamine <aamine@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2005-09-18 21:41:57 +0000
committeraamine <aamine@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2005-09-18 21:41:57 +0000
commitb6c5814f2c4202111de6644636f7ffac4615610a (patch)
tree62bb3c0eacd7d94a23e44ff6472319d7bc912be1 /lib
parent8621cdd900a42c9ab7f0cd53cbc3ac94fb3f2f5d (diff)
* lib/fileutils.rb: backported from trunk (rev 1.65):
* lib/fileutils.rb (rm_r): new option :secure. * lib/fileutils.rb (rm_rf): new option :secure. * lib/fileutils.rb: new method #remove_entry_secure. * lib/fileutils.rb (cd): remove option :noop. * lib/fileutils.rb (cp_r): new option :dereference_root. * lib/fileutils.rb (cp_r): new option :dereference_root. * lib/fileutils.rb: new method #remove_entry. * lib/fileutils.rb: new method #chmod_R. * lib/fileutils.rb: new method #chown. * lib/fileutils.rb: new method #chown_R. * lib/fileutils.rb: new method .commands. * lib/fileutils.rb: new method .options. * lib/fileutils.rb: new method .have_option?. * lib/fileutils.rb: new method .options_of. * lib/fileutils.rb: new method .collect_method. * lib/fileutils.rb: use module_function instead of single extend. * test/fileutils/test_fileutils.rb: backported from trunk (1.36). git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_1_8@9217 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib')
-rw-r--r--lib/fileutils.rb1208
1 files changed, 856 insertions, 352 deletions
diff --git a/lib/fileutils.rb b/lib/fileutils.rb
index 931eabff8b..3465104d58 100644
--- a/lib/fileutils.rb
+++ b/lib/fileutils.rb
@@ -16,8 +16,11 @@
# cd(dir, options) {|dir| .... }
# pwd()
# mkdir(dir, options)
+# mkdir(list, options)
# mkdir_p(dir, options)
+# mkdir_p(list, options)
# rmdir(dir, options)
+# rmdir(list, options)
# ln(old, new, options)
# ln(list, destdir, options)
# ln_s(old, new, options)
@@ -34,6 +37,9 @@
# rm_rf(list, options)
# install(src, dest, mode = <src's>, options)
# chmod(mode, list, options)
+# chmod_R(mode, list, options)
+# chown(user, group, list, options)
+# chown_R(user, group, list, options)
# touch(list, options)
#
# The <tt>options</tt> parameter is a hash of options, taken from the list
@@ -45,13 +51,14 @@
# either one file or a list of files in that argument. See the method
# documentation for examples.
#
-# There are some `low level' methods, which does not accept any option:
+# There are some `low level' methods, which do not accept any option:
#
# copy_entry(src, dest, preserve = false, dereference = false)
# copy_file(src, dest, preserve = false, dereference = true)
# copy_stream(srcstream, deststream)
+# remove_entry(path, force = false)
+# remove_entry_secure(path, force = false)
# remove_file(path, force = false)
-# remove_dir(path, force = false)
# compare_file(path_a, path_b)
# compare_stream(stream_a, stream_b)
# uptodate?(file, cmp_list)
@@ -75,15 +82,15 @@
# <tt>:verbose</tt> flags to methods in FileUtils.
#
-unless defined?(Errno::EXDEV)
- module Errno
- class EXDEV < SystemCallError; end
- end
-end
-
module FileUtils
- # All methods are module_function.
+ def self.private_module_function(name) #:nodoc:
+ module_function name
+ private_class_method name
+ end
+
+ # This hash table holds command options.
+ OPT_TABLE = {} #:nodoc: internal use only
#
# Options: (none)
@@ -93,11 +100,13 @@ module FileUtils
def pwd
Dir.pwd
end
+ module_function :pwd
alias getwd pwd
+ module_function :getwd
#
- # Options: noop verbose
+ # Options: verbose
#
# Changes the current directory to the directory +dir+.
#
@@ -107,13 +116,18 @@ module FileUtils
# FileUtils.cd('/', :verbose => true) # chdir and report it
#
def cd(dir, options = {}, &block) # :yield: dir
- fu_check_options options, :noop, :verbose
+ fu_check_options options, :verbose
fu_output_message "cd #{dir}" if options[:verbose]
Dir.chdir(dir, &block) unless options[:noop]
fu_output_message 'cd -' if options[:verbose] and block
end
+ module_function :cd
alias chdir cd
+ module_function :chdir
+
+ OPT_TABLE['cd'] =
+ OPT_TABLE['chdir'] = %w( verbose )
#
# Options: (none)
@@ -136,7 +150,7 @@ module FileUtils
end
true
end
-
+ module_function :uptodate?
#
# Options: mode noop verbose
@@ -158,6 +172,9 @@ module FileUtils
fu_mkdir dir, options[:mode]
end
end
+ module_function :mkdir
+
+ OPT_TABLE['mkdir'] = %w( noop verbose mode )
#
# Options: mode noop verbose
@@ -206,11 +223,18 @@ module FileUtils
return *list
end
+ module_function :mkdir_p
alias mkpath mkdir_p
alias makedirs mkdir_p
+ module_function :mkpath
+ module_function :makedirs
- def fu_mkdir(path, mode)
+ OPT_TABLE['mkdir_p'] =
+ OPT_TABLE['mkpath'] =
+ OPT_TABLE['makedirs'] = %w( noop verbose )
+
+ def fu_mkdir(path, mode) #:nodoc:
path = path.sub(%r</\z>, '')
if mode
Dir.mkdir path, mode
@@ -219,7 +243,7 @@ module FileUtils
Dir.mkdir path
end
end
- private :fu_mkdir
+ private_module_function :fu_mkdir
#
# Options: noop, verbose
@@ -236,16 +260,18 @@ module FileUtils
list = fu_list(list)
fu_output_message "rmdir #{list.join ' '}" if options[:verbose]
return if options[:noop]
-
list.each do |dir|
Dir.rmdir dir.sub(%r</\z>, '')
end
end
+ module_function :rmdir
+
+ OPT_TABLE['rmdir'] = %w( noop verbose )
#
# Options: force noop verbose
#
- # <b><tt>ln( old, new, options = {} )</tt></b>
+ # <b><tt>ln(old, new, options = {})</tt></b>
#
# Creates a hard link +new+ which points to +old+.
# If +new+ already exists and it is a directory, creates a symbolic link +new/old+.
@@ -255,32 +281,36 @@ module FileUtils
# FileUtils.ln 'gcc', 'cc', :verbose => true
# FileUtils.ln '/usr/bin/emacs21', '/usr/bin/emacs'
#
- # <b><tt>ln( list, destdir, options = {} )</tt></b>
+ # <b><tt>ln(list, destdir, options = {})</tt></b>
#
# Creates several hard links in a directory, with each one pointing to the
# item in +list+. If +destdir+ is not a directory, raises Errno::ENOTDIR.
#
# include FileUtils
- # cd '/bin'
- # ln %w(cp mv mkdir), '/usr/bin' # Now /usr/bin/cp and /bin/cp are linked.
+ # cd '/sbin'
+ # FileUtils.ln %w(cp mv mkdir), '/bin' # Now /sbin/cp and /bin/cp are linked.
#
def ln(src, dest, options = {})
fu_check_options options, :force, :noop, :verbose
fu_output_message "ln#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
return if options[:noop]
-
fu_each_src_dest0(src, dest) do |s,d|
remove_file d, true if options[:force]
File.link s, d
end
end
+ module_function :ln
alias link ln
+ module_function :link
+
+ OPT_TABLE['ln'] =
+ OPT_TABLE['link'] = %w( noop verbose force )
#
# Options: force noop verbose
#
- # <b><tt>ln_s( old, new, options = {} )</tt></b>
+ # <b><tt>ln_s(old, new, options = {})</tt></b>
#
# Creates a symbolic link +new+ which points to +old+. If +new+ already
# exists and it is a directory, creates a symbolic link +new/old+. If +new+
@@ -290,7 +320,7 @@ module FileUtils
# FileUtils.ln_s '/usr/bin/ruby', '/usr/local/bin/ruby'
# FileUtils.ln_s 'verylongsourcefilename.c', 'c', :force => true
#
- # <b><tt>ln_s( list, destdir, options = {} )</tt></b>
+ # <b><tt>ln_s(list, destdir, options = {})</tt></b>
#
# Creates several symbolic links in a directory, with each one pointing to the
# item in +list+. If +destdir+ is not a directory, raises Errno::ENOTDIR.
@@ -303,14 +333,18 @@ module FileUtils
fu_check_options options, :force, :noop, :verbose
fu_output_message "ln -s#{options[:force] ? 'f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
return if options[:noop]
-
fu_each_src_dest0(src, dest) do |s,d|
remove_file d, true if options[:force]
File.symlink s, d
end
end
+ module_function :ln_s
alias symlink ln_s
+ module_function :symlink
+
+ OPT_TABLE['ln_s'] =
+ OPT_TABLE['symlink'] = %w( noop verbose force )
#
# Options: noop verbose
@@ -324,33 +358,41 @@ module FileUtils
options[:force] = true
ln_s src, dest, options
end
+ module_function :ln_sf
+
+ OPT_TABLE['ln_sf'] = %w( noop verbose )
#
# Options: preserve noop verbose
#
- # Copies a file +src+ to +dest+. If +dest+ is a directory, copies
- # +src+ to +dest/src+.
+ # Copies a file content +src+ to +dest+. If +dest+ is a directory,
+ # copies +src+ to +dest/src+.
#
# If +src+ is a list of files, then +dest+ must be a directory.
#
# FileUtils.cp 'eval.c', 'eval.c.org'
# FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6'
# FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true
+ # FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink
#
def cp(src, dest, options = {})
fu_check_options options, :preserve, :noop, :verbose
fu_output_message "cp#{options[:preserve] ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
return if options[:noop]
-
- fu_each_src_dest(src, dest) do |s,d|
+ fu_each_src_dest(src, dest) do |s, d|
copy_file s, d, options[:preserve]
end
end
+ module_function :cp
alias copy cp
+ module_function :copy
+
+ OPT_TABLE['cp'] =
+ OPT_TABLE['copy'] = %w( noop verbose preserve )
#
- # Options: preserve noop verbose
+ # Options: preserve noop verbose dereference_root
#
# Copies +src+ to +dest+. If +src+ is a directory, this method copies
# all its contents recursively. If +dest+ is a directory, copies
@@ -373,61 +415,50 @@ module FileUtils
# # but this doesn't.
#
def cp_r(src, dest, options = {})
- fu_check_options options, :preserve, :noop, :verbose
+ fu_check_options options, :preserve, :noop, :verbose, :dereference_root
fu_output_message "cp -r#{options[:preserve] ? 'p' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
return if options[:noop]
-
- fu_each_src_dest(src, dest) do |s,d|
- if File.directory?(s)
- fu_traverse(s) {|rel, deref, st|
- ctx = CopyContext_.new(options[:preserve], deref, st)
- ctx.copy_entry "#{s}/#{rel}", "#{d}/#{rel}"
- }
- else
- copy_file s, d, options[:preserve]
- end
+ fu_each_src_dest(src, dest) do |s, d|
+ copy_entry s, d, options[:preserve], options[:dereference_root]
end
end
+ module_function :cp_r
- def fu_traverse(prefix, dereference_root = true) #:nodoc:
- stack = ['.']
- deref = dereference_root
- while rel = stack.pop
- st = File.lstat("#{prefix}/#{rel}")
- if st.directory? and (deref or not st.symlink?)
- stack.concat Dir.entries("#{prefix}/#{rel}")\
- .reject {|ent| ent == '.' or ent == '..' }\
- .map {|ent| "#{rel}/#{ent.untaint}" }.reverse
- end
- yield rel, deref, st
- deref = false
- end
- end
- private :fu_traverse
+ OPT_TABLE['cp_r'] = %w( noop verbose preserve dereference_root )
#
# Copies a file system entry +src+ to +dest+.
- # This method preserves file types, c.f. FIFO, device files, directory....
+ # If +src+ is a directory, this method copies its contents recursively.
+ # This method preserves file types, c.f. symlink, directory...
+ # (FIFO, device files and etc. are not supported yet)
#
# Both of +src+ and +dest+ must be a path name.
# +src+ must exist, +dest+ must not exist.
#
# If +preserve+ is true, this method preserves owner, group, permissions
# and modified time.
- # If +dereference+ is true, this method copies a target of symbolic link
- # instead of a symbolic link itself.
#
- def copy_entry(src, dest, preserve = false, dereference = false)
- CopyContext_.new(preserve, dereference).copy_entry src, dest
+ # If +dereference_root+ is true, this method dereference tree root.
+ #
+ def copy_entry(src, dest, preserve = false, dereference_root = false)
+ Entry_.new(src, nil, dereference_root).traverse do |ent|
+ destent = Entry_.new(dest, ent.rel, false)
+ ent.copy destent.path
+ ent.copy_metadata destent.path if preserve
+ end
end
+ module_function :copy_entry
#
# Copies file contents of +src+ to +dest+.
# Both of +src+ and +dest+ must be a path name.
#
def copy_file(src, dest, preserve = false, dereference = true)
- CopyContext_.new(preserve, dereference).copy_content src, dest
+ ent = Entry_.new(src, nil, dereference)
+ ent.copy_file dest
+ ent.copy_metadata dest if preserve
end
+ module_function :copy_file
#
# Copies stream +src+ to +dest+.
@@ -437,127 +468,7 @@ module FileUtils
def copy_stream(src, dest)
fu_copy_stream0 src, dest, fu_stream_blksize(src, dest)
end
-
- def fu_copy_stream0(src, dest, blksize) #:nodoc:
- while s = src.read(blksize)
- dest.write s
- end
- end
- private :fu_copy_stream0
-
- class CopyContext_ #:nodoc: internal use only
- include ::FileUtils
-
- def initialize(preserve = false, dereference = false, stat = nil)
- @preserve = preserve
- @dereference = dereference
- @stat = stat
- end
-
- def copy_entry(src, dest)
- preserve(src, dest) {
- _copy_entry src, dest
- }
- end
-
- def copy_content(src, dest)
- preserve(src, dest) {
- _copy_content src, dest
- }
- end
-
- private
-
- def _copy_entry(src, dest)
- st = stat(src)
- case
- when st.file?
- _copy_content src, dest
- when st.directory?
- begin
- Dir.mkdir File.expand_path(dest)
- rescue => err
- raise unless File.directory?(dest)
- end
- when st.symlink?
- File.symlink File.readlink(src), dest
- when st.chardev?
- raise "cannot handle device file" unless File.respond_to?(:mknod)
- mknod dest, ?c, 0666, st.rdev
- when st.blockdev?
- raise "cannot handle device file" unless File.respond_to?(:mknod)
- mknod dest, ?b, 0666, st.rdev
- when st.socket?
- raise "cannot handle socket" unless File.respond_to?(:mknod)
- mknod dest, nil, st.mode, 0
- when st.pipe?
- raise "cannot handle FIFO" unless File.respond_to?(:mkfifo)
- mkfifo dest, 0666
- when (st.mode & 0xF000) == (_S_IF_DOOR = 0xD000) # door
- raise "cannot handle door: #{src}"
- else
- raise "unknown file type: #{src}"
- end
- end
-
- def _copy_content(src, dest)
- st = stat(src)
- File.open(src, 'rb') {|r|
- File.open(dest, 'wb', st.mode) {|w|
- fu_copy_stream0 r, w, (fu_blksize(st) || fu_default_blksize())
- }
- }
- end
-
- def preserve(src, dest)
- return yield unless @preserve
- st = stat(src)
- yield
- File.utime st.atime, st.mtime, dest
- begin
- chown st.uid, st.gid, dest
- rescue Errno::EPERM
- # clear setuid/setgid
- chmod st.mode & 01777, dest
- else
- chmod st.mode, dest
- end
- end
-
- def stat(path)
- if @dereference
- @stat ||= ::File.stat(path)
- else
- @stat ||= ::File.lstat(path)
- end
- end
-
- def chmod(mode, path)
- if @dereference
- ::File.chmod mode, path
- else
- begin
- ::File.lchmod mode, path
- rescue NotImplementedError
- # just ignore this because chmod(symlink) changes attributes of
- # symlink target, which is not our intent.
- end
- end
- end
-
- def chown(uid, gid, path)
- if @dereference
- ::File.chown uid, gid, path
- else
- begin
- ::File.lchown uid, gid, path
- rescue NotImplementedError
- # just ignore this because chown(symlink) changes attributes of
- # symlink target, which is not our intent.
- end
- end
- end
- end
+ module_function :copy_stream
#
# Options: force noop verbose
@@ -575,16 +486,15 @@ module FileUtils
fu_check_options options, :force, :noop, :verbose
fu_output_message "mv#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
return if options[:noop]
-
- fu_each_src_dest(src, dest) do |s,d|
- src_stat = fu_lstat(s)
- dest_stat = fu_stat(d)
+ fu_each_src_dest(src, dest) do |s, d|
+ destent = Entry_.new(d, nil, true)
begin
- if rename_cannot_overwrite_file? and dest_stat and not dest_stat.directory?
- File.unlink d
- end
- if dest_stat and dest_stat.directory?
- raise Errno::EISDIR, dest
+ if destent.exist?
+ if destent.directory?
+ raise Errno::EEXIST, dest
+ else
+ destent.remove_file if rename_cannot_overwrite_file?
+ end
end
begin
File.rename s, d
@@ -596,27 +506,18 @@ module FileUtils
end
end
end
+ module_function :mv
alias move mv
+ module_function :move
- def fu_stat(path)
- File.stat(path)
- rescue SystemCallError
- nil
- end
- private :fu_stat
-
- def fu_lstat(path)
- File.lstat(path)
- rescue SystemCallError
- nil
- end
- private :fu_lstat
+ OPT_TABLE['mv'] =
+ OPT_TABLE['move'] = %w( noop verbose force )
def rename_cannot_overwrite_file? #:nodoc:
/djgpp|cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
end
- private :rename_cannot_overwrite_file?
+ private_module_function :rename_cannot_overwrite_file?
#
# Options: force noop verbose
@@ -634,18 +535,24 @@ module FileUtils
fu_output_message "rm#{options[:force] ? ' -f' : ''} #{list.join ' '}" if options[:verbose]
return if options[:noop]
- list.each do |fname|
- remove_file fname, options[:force]
+ list.each do |path|
+ remove_file path, options[:force]
end
end
+ module_function :rm
alias remove rm
+ module_function :remove
+
+ OPT_TABLE['rm'] =
+ OPT_TABLE['remove'] = %w( noop verbose force )
#
# Options: noop verbose
#
- # Same as
- # #rm(list, :force)
+ # Equivalent to
+ #
+ # #rm(list, :force => true)
#
def rm_f(list, options = {})
fu_check_options options, :noop, :verbose
@@ -653,11 +560,16 @@ module FileUtils
options[:force] = true
rm list, options
end
+ module_function :rm_f
alias safe_unlink rm_f
+ module_function :safe_unlink
+
+ OPT_TABLE['rm_f'] =
+ OPT_TABLE['safe_unlink'] = %w( noop verbose )
#
- # Options: force noop verbose
+ # Options: force noop verbose secure
#
# remove files +list+[0] +list+[1]... If +list+[n] is a directory,
# removes its all contents recursively. This method ignores
@@ -665,94 +577,200 @@ module FileUtils
#
# FileUtils.rm_r Dir.glob('/tmp/*')
# FileUtils.rm_r '/', :force => true # :-)
+ #
+ # WARNING: This method causes local vulnerability
+ # if one of parent directories or removing directory tree are world
+ # writable (including /tmp, whose permission is 1777), and the current
+ # process has strong privilege such as Unix super user (root), and the
+ # system has symbolic link. For secure removing, read the documentation
+ # of #remove_entry_secure carefully, and set :secure option to true.
+ # Default is :secure=>false.
+ #
+ # NOTE: This method calls #remove_entry_secure if :secure option is set.
+ # See also #remove_entry_secure.
#
def rm_r(list, options = {})
- fu_check_options options, :force, :noop, :verbose
+ fu_check_options options, :force, :noop, :verbose, :secure
+ # options[:secure] = true unless options.key?(:secure)
list = fu_list(list)
fu_output_message "rm -r#{options[:force] ? 'f' : ''} #{list.join ' '}" if options[:verbose]
return if options[:noop]
-
- list.each do |fname|
- begin
- st = File.lstat(fname)
- rescue
- next if options[:force]
- raise
- end
- if st.symlink? then remove_file fname, options[:force]
- elsif st.directory? then remove_dir fname, options[:force]
- else remove_file fname, options[:force]
+ list.each do |path|
+ if options[:secure]
+ remove_entry_secure path, options[:force]
+ else
+ remove_entry path, options[:force]
end
end
end
+ module_function :rm_r
+
+ OPT_TABLE['rm_r'] = %w( noop verbose force secure )
#
- # Options: noop verbose
+ # Options: noop verbose secure
#
- # Same as
+ # Equivalent to
+ #
# #rm_r(list, :force => true)
+ #
+ # WARNING: This method causes local vulnerability.
+ # Read the documentation of #rm_r first.
#
def rm_rf(list, options = {})
- fu_check_options options, :noop, :verbose
+ fu_check_options options, :noop, :verbose, :secure
options = options.dup
options[:force] = true
rm_r list, options
end
+ module_function :rm_rf
alias rmtree rm_rf
+ module_function :rmtree
+
+ OPT_TABLE['rm_rf'] =
+ OPT_TABLE['rmtree'] = %w( noop verbose secure )
+
+ #
+ # This method removes a file system entry +path+. +path+ shall be a
+ # regular file, a directory, or something. If +path+ is a directory,
+ # remove it recursively. This method is required to avoid TOCTTOU
+ # (time-of-check-to-time-of-use) local security vulnerability of #rm_r.
+ # #rm_r causes security hole when:
+ #
+ # * Parent directory is world writable (including /tmp).
+ # * Removing directory tree includes world writable directory.
+ # * The system has symbolic link.
+ #
+ # To avoid this security hole, this method applies special preprocess.
+ # If +path+ is a directory, this method chown(2) and chmod(2) all
+ # removing directories. This requires the current process is the
+ # owner of the removing whole directory tree, or is the super user (root).
+ #
+ # WARNING: You must ensure that *ALL* parent directories are not
+ # world writable. Otherwise this method does not work.
+ # Only exception is temporary directory like /tmp and /var/tmp,
+ # whose permission is 1777.
+ #
+ # WARNING: Only the owner of the removing directory tree, or Unix super
+ # user (root) should invoke this method. Otherwise this method does not
+ # work.
+ #
+ # For details of this security vulnerability, see Perl's case:
+ #
+ # http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448
+ # http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452
+ #
+ # For fileutils.rb, this vulnerability is reported in [ruby-dev:26100].
+ #
+ def remove_entry_secure(path, force = false)
+ unless fu_have_symlink?
+ remove_entry path, force
+ return
+ end
+ fullpath = File.expand_path(path)
+ st = File.lstat(fullpath)
+ unless st.directory?
+ File.unlink fullpath
+ return
+ end
+ # is a directory.
+ parent_st = File.stat(File.dirname(fullpath))
+ unless fu_world_writable?(parent_st)
+ remove_entry path, force
+ return
+ end
+ unless parent_st.sticky?
+ raise ArgumentError, "parent directory is world writable, FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})"
+ end
+ # freeze tree root
+ euid = Process.euid
+ File.open(fullpath + '/.') {|f|
+ unless fu_stat_identical_entry?(st, f.stat)
+ # symlink (TOC-to-TOU attack?)
+ File.unlink fullpath
+ return
+ end
+ f.chown euid, -1
+ f.chmod 0700
+ }
+ # ---- tree root is frozen ----
+ root = Entry_.new(path)
+ root.preorder_traverse do |ent|
+ if ent.directory?
+ ent.chown euid, -1
+ ent.chmod 0700
+ end
+ end
+ root.postorder_traverse do |ent|
+ begin
+ ent.remove
+ rescue
+ raise unless force
+ end
+ end
+ rescue
+ raise unless force
+ end
+ module_function :remove_entry_secure
+
+ def fu_world_writable?(st)
+ (st.mode & 0002) != 0
+ end
+
+ def fu_have_symlink? #:nodoc
+ File.symlink nil, nil
+ rescue NotImplementedError
+ return false
+ rescue
+ return true
+ end
+ private_module_function :fu_have_symlink?
+
+ def fu_stat_identical_entry?(a, b) #:nodoc:
+ a.dev == b.dev and a.ino == b.ino
+ end
+ private_module_function :fu_stat_identical_entry?
+
+ #
+ # This method removes a file system entry +path+.
+ # +path+ might be a regular file, a directory, or something.
+ # If +path+ is a directory, remove it recursively.
+ #
+ # See also #remove_entry_secure.
+ #
+ def remove_entry(path, force = false)
+ Entry_.new(path).postorder_traverse do |ent|
+ begin
+ ent.remove
+ rescue
+ raise unless force
+ end
+ end
+ rescue
+ raise unless force
+ end
+ module_function :remove_entry
+ #
# Removes a file +path+.
# This method ignores StandardError if +force+ is true.
+ #
def remove_file(path, force = false)
- first_time_p = true
- begin
- File.unlink path
- rescue Errno::ENOENT
- raise unless force
- rescue => err
- if first_time_p
- first_time_p = false
- begin
- File.chmod 0777, path
- retry
- rescue SystemCallError
- end
- end
- raise err unless force
- end
+ Entry_.new(path).remove_file
+ rescue
+ raise unless force
end
+ module_function :remove_file
+ #
# Removes a directory +dir+ and its contents recursively.
# This method ignores StandardError if +force+ is true.
- def remove_dir(dir, force = false)
- Dir.foreach(dir) do |file|
- next if /\A\.\.?\z/ =~ file
- path = "#{dir}/#{file.untaint}"
- if File.symlink?(path)
- remove_file path, force
- elsif File.directory?(path)
- remove_dir path, force
- else
- remove_file path, force
- end
- end
- first_time_p = true
- begin
- Dir.rmdir dir.sub(%r</\z>, '')
- rescue Errno::ENOENT
- raise unless force
- rescue => err
- if first_time_p
- first_time_p = false
- begin
- File.chmod 0777, dir
- retry
- rescue SystemCallError
- end
- end
- raise err unless force
- end
+ #
+ def remove_dir(path, force = false)
+ remove_entry path, force # FIXME?? check if it is a directory
end
+ module_function :remove_dir
#
# Returns true if the contents of a file A and a file B are identical.
@@ -768,9 +786,12 @@ module FileUtils
}
}
end
+ module_function :compare_file
alias identical? compare_file
alias cmp compare_file
+ module_function :identical?
+ module_function :cmp
#
# Returns true if the contents of a stream +a+ and +b+ are identical.
@@ -789,6 +810,7 @@ module FileUtils
end
false
end
+ module_function :compare_stream
#
# Options: mode noop verbose
@@ -803,9 +825,8 @@ module FileUtils
fu_check_options options, :mode, :preserve, :noop, :verbose
fu_output_message "install -c#{options[:preserve] && ' -p'}#{options[:mode] ? (' -m 0%o' % options[:mode]) : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
return if options[:noop]
-
- fu_each_src_dest(src, dest) do |s,d|
- unless File.exist?(d) and compare_file(s,d)
+ fu_each_src_dest(src, dest) do |s, d|
+ unless File.exist?(d) and compare_file(s, d)
remove_file d, true
st = File.stat(s) if options[:preserve]
copy_file s, d
@@ -814,6 +835,9 @@ module FileUtils
end
end
end
+ module_function :install
+
+ OPT_TABLE['install'] = %w( noop verbose preserve mode )
#
# Options: noop verbose
@@ -830,7 +854,143 @@ module FileUtils
list = fu_list(list)
fu_output_message sprintf('chmod %o %s', mode, list.join(' ')) if options[:verbose]
return if options[:noop]
- File.chmod mode, *list
+ list.each do |path|
+ Entry_.new(path).chmod mode
+ end
+ end
+ module_function :chmod
+
+ OPT_TABLE['chmod'] = %w( noop verbose )
+
+ #
+ # Options: noop verbose force
+ #
+ # Changes permission bits on the named files (in +list+)
+ # to the bit pattern represented by +mode+.
+ #
+ # FileUtils.chmod_R 0700, "/tmp/app.#{$$}"
+ #
+ def chmod_R(mode, list, options = {})
+ fu_check_options options, :noop, :verbose, :force
+ list = fu_list(list)
+ fu_output_message sprintf('chmod -R%s %o %s',
+ (options[:force] ? 'f' : ''),
+ mode, list.join(' ')) if options[:verbose]
+ return if options[:noop]
+ list.each do |root|
+ Entry_.new(root).traverse do |ent|
+ begin
+ ent.chmod mode
+ rescue
+ raise unless options[:force]
+ end
+ end
+ end
+ end
+ module_function :chmod_R
+
+ OPT_TABLE['chmod_R'] = %w( noop verbose )
+
+ #
+ # Options: noop verbose
+ #
+ # Changes owner and group on the named files (in +list+)
+ # to the user +user+ and the group +group+. +user+ and +group+
+ # may be an ID (Integer/String) or a name (String).
+ # If +user+ or +group+ is nil, this method does not change
+ # the attribute.
+ #
+ # FileUtils.chown 'root', 'staff', '/usr/local/bin/ruby'
+ # FileUtils.chown nil, 'bin', Dir.glob('/usr/bin/*'), :verbose => true
+ #
+ def chown(user, group, list, options = {})
+ fu_check_options options, :noop, :verbose
+ list = fu_list(list)
+ fu_output_message sprintf('chown %s%s',
+ [user,group].compact.join(':') + ' ',
+ list.join(' ')) if options[:verbose]
+ return if options[:noop]
+ uid = fu_get_uid(user)
+ gid = fu_get_gid(group)
+ list.each do |path|
+ Entry_.new(path).chown uid, gid
+ end
+ end
+ module_function :chown
+
+ OPT_TABLE['chown'] = %w( noop verbose )
+
+ #
+ # Options: noop verbose force
+ #
+ # Changes owner and group on the named files (in +list+)
+ # to the user +user+ and the group +group+ recursively.
+ # +user+ and +group+ may be an ID (Integer/String) or
+ # a name (String). If +user+ or +group+ is nil, this
+ # method does not change the attribute.
+ #
+ # FileUtils.chown_R 'www', 'www', '/var/www/htdocs'
+ # FileUtils.chown_R 'cvs', 'cvs', '/var/cvs', :verbose => true
+ #
+ def chown_R(user, group, list, options = {})
+ fu_check_options options, :noop, :verbose, :force
+ list = fu_list(list)
+ fu_output_message sprintf('chown -R%s %s%s',
+ (options[:force] ? 'f' : ''),
+ [user,group].compact.join(':') + ' ',
+ list.join(' ')) if options[:verbose]
+ return if options[:noop]
+ uid = fu_get_uid(user)
+ gid = fu_get_gid(group)
+ return unless uid or gid
+ list.each do |root|
+ Entry_.new(root).traverse do |ent|
+ begin
+ ent.chown uid, gid
+ rescue
+ raise unless options[:force]
+ end
+ end
+ end
+ end
+ module_function :chown_R
+
+ OPT_TABLE['chown_R'] = %w( noop verbose )
+
+ begin
+ require 'etc'
+
+ def fu_get_uid(user) #:nodoc:
+ return nil unless user
+ user = user.to_s
+ if /\A\d+\z/ =~ user
+ then user.to_i
+ else Etc.getpwnam(user).uid
+ end
+ end
+ private_module_function :fu_get_uid
+
+ def fu_get_gid(group) #:nodoc:
+ return nil unless group
+ if /\A\d+\z/ =~ group
+ then group.to_i
+ else Etc.getgrnam(group).gid
+ end
+ end
+ private_module_function :fu_get_gid
+
+ rescue LoadError
+ # need Win32 support???
+
+ def fu_get_uid(user) #:nodoc:
+ user # FIXME
+ end
+ private_module_function :fu_get_uid
+
+ def fu_get_gid(group) #:nodoc:
+ group # FIXME
+ end
+ private_module_function :fu_get_gid
end
#
@@ -847,56 +1007,396 @@ module FileUtils
list = fu_list(list)
fu_output_message "touch #{list.join ' '}" if options[:verbose]
return if options[:noop]
-
t = Time.now
- list.each do |fname|
+ list.each do |path|
begin
- File.utime(t, t, fname)
+ File.utime(t, t, path)
rescue Errno::ENOENT
- File.open(fname, 'a') {
+ File.open(path, 'a') {
;
}
end
end
end
+ module_function :touch
+
+ OPT_TABLE['touch'] = %w( noop verbose )
private
- def fu_check_options(options, *optdecl)
- h = options.dup
- optdecl.each do |name|
- h.delete name
+ module StreamUtils_
+ private
+
+ def fu_windows?
+ /mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
+ end
+
+ def fu_copy_stream0(src, dest, blksize) #:nodoc:
+ # FIXME: readpartial?
+ while s = src.read(blksize)
+ dest.write s
+ end
+ end
+
+ def fu_stream_blksize(*streams)
+ streams.each do |s|
+ next unless s.respond_to?(:stat)
+ size = fu_blksize(s.stat)
+ return size if size
+ end
+ fu_default_blksize()
+ end
+
+ def fu_blksize(st)
+ s = st.blksize
+ return nil unless s
+ return nil if s == 0
+ s
+ end
+
+ def fu_default_blksize
+ 1024
end
- raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless h.empty?
end
- def fu_list(arg)
+ include StreamUtils_
+ extend StreamUtils_
+
+ class Entry_ #:nodoc: internal use only
+ include StreamUtils_
+
+ def initialize(a, b = nil, deref = false)
+ @prefix = @rel = @path = nil
+ if b
+ @prefix = a
+ @rel = b
+ else
+ @path = a
+ end
+ @deref = deref
+ @stat = nil
+ @lstat = nil
+ end
+
+ def inspect
+ "\#<#{self.class} #{path()}>"
+ end
+
+ def path
+ if @path
+ @path.to_str
+ else
+ join(@prefix, @rel)
+ end
+ end
+
+ def prefix
+ @prefix || @path
+ end
+
+ def rel
+ @rel
+ end
+
+ def dereference?
+ @deref
+ end
+
+ def exist?
+ lstat! ? true : false
+ end
+
+ def file?
+ s = lstat!
+ s and s.file?
+ end
+
+ def directory?
+ s = lstat!
+ s and s.directory?
+ end
+
+ def symlink?
+ s = lstat!
+ s and s.symlink?
+ end
+
+ def chardev?
+ s = lstat!
+ s and s.chardev?
+ end
+
+ def blockdev?
+ s = lstat!
+ s and s.blockdev?
+ end
+
+ def socket?
+ s = lstat!
+ s and s.socket?
+ end
+
+ def pipe?
+ s = lstat!
+ s and s.pipe?
+ end
+
+ S_IF_DOOR = 0xD000
+
+ def door?
+ s = lstat!
+ s and (s.mode & 0xF000 == S_IF_DOOR)
+ end
+
+ def entries
+ Dir.entries(path())\
+ .reject {|n| n == '.' or n == '..' }\
+ .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) }
+ end
+
+ def stat
+ return @stat if @stat
+ if lstat() and lstat().symlink?
+ @stat = File.stat(path())
+ else
+ @stat = lstat()
+ end
+ @stat
+ end
+
+ def stat!
+ return @stat if @stat
+ if lstat! and lstat!.symlink?
+ @stat = File.stat(path())
+ else
+ @stat = lstat!
+ end
+ @stat
+ rescue SystemCallError
+ nil
+ end
+
+ def lstat
+ if dereference?
+ @lstat ||= File.stat(path())
+ else
+ @lstat ||= File.lstat(path())
+ end
+ end
+
+ def lstat!
+ lstat()
+ rescue SystemCallError
+ nil
+ end
+
+ def chmod(mode)
+ if symlink?
+ File.lchmod mode, path() if have_lchmod?
+ else
+ File.chmod mode, path()
+ end
+ end
+
+ def chown(uid, gid)
+ if symlink?
+ File.lchown uid, gid, path() if have_lchown?
+ else
+ File.chown uid, gid, path()
+ end
+ end
+
+ def copy(dest)
+ case
+ when file?
+ copy_file dest
+ when directory?
+ begin
+ Dir.mkdir dest
+ rescue
+ raise unless File.directory?(dest)
+ end
+ when symlink?
+ File.symlink File.readlink(path()), dest
+ when chardev?
+ raise "cannot handle device file" unless File.respond_to?(:mknod)
+ mknod dest, ?c, 0666, lstat().rdev
+ when blockdev?
+ raise "cannot handle device file" unless File.respond_to?(:mknod)
+ mknod dest, ?b, 0666, lstat().rdev
+ when socket?
+ raise "cannot handle socket" unless File.respond_to?(:mknod)
+ mknod dest, nil, lstat().mode, 0
+ when pipe?
+ raise "cannot handle FIFO" unless File.respond_to?(:mkfifo)
+ mkfifo dest, 0666
+ when door?
+ raise "cannot handle door: #{path()}"
+ else
+ raise "unknown file type: #{path()}"
+ end
+ end
+
+ def copy_file(dest)
+ st = stat()
+ File.open(path(), 'rb') {|r|
+ File.open(dest, 'wb', st.mode) {|w|
+ fu_copy_stream0 r, w, (fu_blksize(st) || fu_default_blksize())
+ }
+ }
+ end
+
+ def copy_metadata(path)
+ st = lstat()
+ File.utime st.atime, st.mtime, path
+ begin
+ File.chown st.uid, st.gid, path
+ rescue Errno::EPERM
+ # clear setuid/setgid
+ File.chmod st.mode & 01777, path
+ else
+ File.chmod st.mode, path
+ end
+ end
+
+ def remove
+ if directory?
+ remove_dir1
+ else
+ remove_file
+ end
+ end
+
+ def remove_dir1
+ platform_support {
+ Dir.rmdir path().sub(%r</\z>, '')
+ }
+ end
+
+ def remove_file
+ platform_support {
+ File.unlink path
+ }
+ end
+
+ def platform_support
+ return yield unless fu_windows?
+ first_time_p = true
+ begin
+ yield
+ rescue Errno::ENOENT
+ raise
+ rescue => err
+ if first_time_p
+ first_time_p = false
+ begin
+ File.chmod 0700, path() # Windows does not have symlink
+ retry
+ rescue SystemCallError
+ end
+ end
+ raise err
+ end
+ end
+
+ def preorder_traverse
+ stack = [self]
+ while ent = stack.pop
+ yield ent
+ stack.concat ent.entries.reverse if ent.directory?
+ end
+ end
+
+ alias traverse preorder_traverse
+
+ def postorder_traverse
+ if directory?
+ entries().each do |ent|
+ ent.postorder_traverse do |e|
+ yield e
+ end
+ end
+ end
+ yield self
+ end
+
+ private
+
+ $fileutils_rb_have_lchmod = nil
+
+ def have_lchmod?
+ # This is not MT-safe, but it does not matter.
+ if $fileutils_rb_have_lchmod == nil
+ $fileutils_rb_have_lchmod = check_have_lchmod?
+ end
+ $fileutils_rb_have_lchmod
+ end
+
+ def check_have_lchmod?
+ return false unless File.respond_to?(:lchmod)
+ File.lchmod 0
+ return true
+ rescue NotImplementedError
+ return false
+ end
+
+ $fileutils_rb_have_lchown = nil
+
+ def have_lchown?
+ # This is not MT-safe, but it does not matter.
+ if $fileutils_rb_have_lchown == nil
+ $fileutils_rb_have_lchown = check_have_lchown?
+ end
+ $fileutils_rb_have_lchown
+ end
+
+ def check_have_lchown?
+ return false unless File.respond_to?(:lchown)
+ File.lchown nil, nil
+ return true
+ rescue NotImplementedError
+ return false
+ end
+
+ def join(dir, base)
+ return dir.to_str if not base or base == '.'
+ return base.to_str if not dir or dir == '.'
+ File.join(dir, base)
+ end
+ end # class Entry_
+
+ def fu_list(arg) #:nodoc:
[arg].flatten.map {|path| path.to_str }
end
+ private_module_function :fu_list
- def fu_each_src_dest(src, dest)
+ def fu_each_src_dest(src, dest) #:nodoc:
fu_each_src_dest0(src, dest) do |s, d|
raise ArgumentError, "same file: #{s} and #{d}" if fu_same?(s, d)
yield s, d
end
end
+ private_module_function :fu_each_src_dest
- def fu_each_src_dest0(src, dest)
+ def fu_each_src_dest0(src, dest) #:nodoc:
if src.is_a?(Array)
src.each do |s|
- yield s.to_str, File.join(dest, File.basename(s))
+ s = s.to_str
+ yield s, File.join(dest, File.basename(s))
end
else
+ src = src.to_str
if File.directory?(dest)
- yield src.to_str, File.join(dest, File.basename(src))
+ yield src, File.join(dest, File.basename(src))
else
- yield src.to_str, dest.to_str
+ yield src, dest.to_str
end
end
end
+ private_module_function :fu_each_src_dest0
- def fu_same?(a, b)
- if have_st_ino?
+ def fu_same?(a, b) #:nodoc:
+ if fu_have_st_ino?
st1 = File.stat(a)
st2 = File.stat(b)
st1.dev == st2.dev and st1.ino == st2.ino
@@ -906,82 +1406,89 @@ module FileUtils
rescue Errno::ENOENT
return false
end
+ private_module_function :fu_same?
- def have_st_ino?
- /mswin|mingw|bccwin|wince|emx/ !~ RUBY_PLATFORM
+ def fu_have_st_ino? #:nodoc:
+ not fu_windows?
end
+ private_module_function :fu_have_st_ino?
- def fu_stream_blksize(*streams)
- streams.each do |s|
- next unless s.respond_to?(:stat)
- size = fu_blksize(s.stat)
- return size if size
+ def fu_check_options(options, *optdecl) #:nodoc:
+ h = options.dup
+ optdecl.each do |name|
+ h.delete name
end
- fu_default_blksize()
- end
-
- def fu_blksize(st)
- s = st.blksize
- return nil unless s
- return nil if s == 0
- s
+ raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless h.empty?
end
+ private_module_function :fu_check_options
- def fu_default_blksize
- 1024
+ def fu_update_option(args, new) #:nodoc:
+ if args.last.is_a?(Hash)
+ args[-1] = args.last.dup.update(new)
+ else
+ args.push new
+ end
+ args
end
+ private_module_function :fu_update_option
@fileutils_output = $stderr
@fileutils_label = ''
- def fu_output_message(msg)
+ def fu_output_message(msg) #:nodoc:
@fileutils_output ||= $stderr
@fileutils_label ||= ''
@fileutils_output.puts @fileutils_label + msg
end
+ private_module_function :fu_output_message
- def fu_update_option(args, new)
- if args.last.is_a?(Hash)
- args.last.update new
- else
- args.push new
- end
- args
+ #
+ # Returns an Array of method names which have any options.
+ #
+ # p FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...]
+ #
+ def FileUtils.commands
+ OPT_TABLE.keys
end
+ #
+ # Returns an Array of option names.
+ #
+ # p FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"]
+ #
+ def FileUtils.options
+ OPT_TABLE.values.flatten.uniq
+ end
- extend self
-
-
- OPT_TABLE = {
- 'cd' => %w( noop verbose ),
- 'chdir' => %w( noop verbose ),
- 'chmod' => %w( noop verbose ),
- 'copy' => %w( noop verbose preserve ),
- 'cp' => %w( noop verbose preserve ),
- 'cp_r' => %w( noop verbose preserve ),
- 'install' => %w( noop verbose preserve mode ),
- 'link' => %w( noop verbose force ),
- 'ln' => %w( noop verbose force ),
- 'ln_s' => %w( noop verbose force ),
- 'ln_sf' => %w( noop verbose ),
- 'makedirs' => %w( noop verbose ),
- 'mkdir' => %w( noop verbose mode ),
- 'mkdir_p' => %w( noop verbose mode ),
- 'mkpath' => %w( noop verbose ),
- 'move' => %w( noop verbose force ),
- 'mv' => %w( noop verbose force ),
- 'remove' => %w( noop verbose force ),
- 'rm' => %w( noop verbose force ),
- 'rm_f' => %w( noop verbose ),
- 'rm_r' => %w( noop verbose force ),
- 'rm_rf' => %w( noop verbose ),
- 'rmtree' => %w( noop verbose ),
- 'rmdir' => %w( noop verbose ),
- 'safe_unlink' => %w( noop verbose ),
- 'symlink' => %w( noop verbose force ),
- 'touch' => %w( noop verbose )
- }
+ #
+ # Returns true if the method +mid+ have an option +opt+.
+ #
+ # p FileUtils.have_option?(:cp, :noop) #=> true
+ # p FileUtils.have_option?(:rm, :force) #=> true
+ # p FileUtils.have_option?(:rm, :perserve) #=> false
+ #
+ def FileUtils.have_option?(mid, opt)
+ li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}"
+ li.include?(opt.to_s)
+ end
+
+ #
+ # Returns an Array of option names of the method +mid+.
+ #
+ # p FileUtils.options(:rm) #=> ["noop", "verbose", "force"]
+ #
+ def FileUtils.options_of(mid)
+ OPT_TABLE[mid.to_s]
+ end
+
+ #
+ # Returns an Array of method names which have the option +opt+.
+ #
+ # p FileUtils.collect_method(:preserve) #=> ["cp", "cp_r", "copy", "install"]
+ #
+ def FileUtils.collect_method(opt)
+ OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt.to_s) }
+ end
#
# This module has all methods of FileUtils module, but it outputs messages
@@ -992,8 +1499,7 @@ module FileUtils
include FileUtils
@fileutils_output = $stderr
@fileutils_label = ''
- FileUtils::OPT_TABLE.each do |name, opts|
- next unless opts.include?('verbose')
+ ::FileUtils.collect_method('verbose').each do |name|
module_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{name}(*args)
super(*fu_update_option(args, :verbose => true))
@@ -1012,8 +1518,7 @@ module FileUtils
include FileUtils
@fileutils_output = $stderr
@fileutils_label = ''
- FileUtils::OPT_TABLE.each do |name, opts|
- next unless opts.include?('noop')
+ ::FileUtils.collect_method('noop').each do |name|
module_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{name}(*args)
super(*fu_update_option(args, :noop => true))
@@ -1033,8 +1538,7 @@ module FileUtils
include FileUtils
@fileutils_output = $stderr
@fileutils_label = ''
- FileUtils::OPT_TABLE.each do |name, opts|
- next unless opts.include?('noop')
+ ::FileUtils.collect_method('noop').each do |name|
module_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{name}(*args)
super(*fu_update_option(args, :noop => true, :verbose => true))