diff --git a/file.c b/file.c index ce367fd..b09c5aa 100644 --- a/file.c +++ b/file.c @@ -93,6 +93,18 @@ int flock(int, int); #include #endif +#ifdef HAVE_SYSCALL_H +# include +#elif defined HAVE_SYS_SYSCALL_H +# include +#endif +#ifndef RENAME_NOREPLACE +# define RENAME_NOREPLACE 1 +#endif +#ifndef RENAME_EXCHANGE +# define RENAME_EXCHANGE 2 +#endif + #if !defined HAVE_LSTAT && !defined lstat #define lstat stat #endif @@ -2839,19 +2851,30 @@ rb_file_s_unlink(int argc, VALUE *argv, VALUE klass) /* * call-seq: * File.rename(old_name, new_name) -> 0 + * File.rename(old_name, new_name, replace: bool) -> 0 * * Renames the given file to the new name. Raises a * SystemCallError if the file cannot be renamed. * * File.rename("afile", "afile.bak") #=> 0 + * + * If replace is false (default is true), new_name will not be + * overwritten and an exception will be raised. */ static VALUE -rb_file_s_rename(VALUE klass, VALUE from, VALUE to) +rb_file_s_rename(int argc, VALUE *argv, VALUE klass) { + static ID keyword_id; const char *src, *dst; - VALUE f, t; + unsigned int flags = 0; + VALUE from, to, f, t, opt, vreplace; + if (!keyword_id) { + CONST_ID(keyword_id, "replace"); + } + + rb_scan_args(argc, argv, "2:", &from, &to, &opt); FilePathValue(from); FilePathValue(to); f = rb_str_encode_ospath(from); @@ -2861,6 +2884,23 @@ rb_file_s_rename(VALUE klass, VALUE from, VALUE to) #if defined __CYGWIN__ errno = 0; #endif + if (!NIL_P(opt)) { + rb_get_kwargs(opt, &keyword_id, 0, 1, &vreplace); + if (vreplace != Qundef && !RTEST(vreplace)) { /* replace: false */ + flags |= RENAME_NOREPLACE; + } + if (flags) { /* use renameat2 */ +#if defined __linux__ && defined __NR_renameat2 + if (syscall(__NR_renameat2, AT_FDCWD, src, AT_FDCWD, dst, flags) < 0) { + int e = errno; + syserr_fail2(e, from, to); + } + return INT2FIX(0); +#else + rb_raise(rb_eNotImpError, "additional flags are not supported"); +#endif + } + } if (rename(src, dst) < 0) { int e = errno; #if defined DOSISH @@ -2880,6 +2920,41 @@ rb_file_s_rename(VALUE klass, VALUE from, VALUE to) /* * call-seq: + * File.exchange(old_name, new_name) -> 0 + * + * Exchange two files atomically. This will cause NotImplementedError if + * renameat2(2) is not available. + */ + +static VALUE +rb_file_s_exchange(VALUE klass, VALUE from, VALUE to) +{ + const char *src, *dst; + VALUE f, t; + + FilePathValue(from); + FilePathValue(to); + f = rb_str_encode_ospath(from); + t = rb_str_encode_ospath(to); + src = StringValueCStr(f); + dst = StringValueCStr(t); +#if defined __CYGWIN__ + errno = 0; +#endif +#if defined __linux__ && defined __NR_renameat2 + if (syscall(__NR_renameat2, AT_FDCWD, src, AT_FDCWD, dst, RENAME_EXCHANGE) < 0) { + int e = errno; + syserr_fail2(e, from, to); + } +#else + rb_raise(rb_eNotImpError, "atomic exchange is not supported"); +#endif + + return INT2FIX(0); +} + +/* + * call-seq: * File.umask() -> integer * File.umask(integer) -> integer * @@ -6015,7 +6090,8 @@ Init_File(void) rb_define_singleton_method(rb_cFile, "unlink", rb_file_s_unlink, -1); rb_define_singleton_method(rb_cFile, "delete", rb_file_s_unlink, -1); - rb_define_singleton_method(rb_cFile, "rename", rb_file_s_rename, 2); + rb_define_singleton_method(rb_cFile, "rename", rb_file_s_rename, -1); + rb_define_singleton_method(rb_cFile, "exchange", rb_file_s_exchange, 2); rb_define_singleton_method(rb_cFile, "umask", rb_file_s_umask, -1); rb_define_singleton_method(rb_cFile, "truncate", rb_file_s_truncate, 2); rb_define_singleton_method(rb_cFile, "mkfifo", rb_file_s_mkfifo, -1); diff --git a/test/ruby/test_file_exhaustive.rb b/test/ruby/test_file_exhaustive.rb index a3c2a40..9711161 100644 --- a/test/ruby/test_file_exhaustive.rb +++ b/test/ruby/test_file_exhaustive.rb @@ -716,6 +716,25 @@ def test_rename end end + def test_rename_replace + assert_equal(0, File.rename(regular_file, nofile, replace: false)) + assert_file.not_exist?(regular_file) + assert_file.exist?(nofile) + assert_equal(0, File.rename(nofile, regular_file, replace: false)) + assert_raise(Errno::EEXIST) { assert_equal(0, File.rename(regular_file, zerofile, replace: false)) } + rescue NotImplementedError + end if POSIX + + def test_exchange + content = File.read(regular_file) + assert_equal(0, File.exchange(regular_file, zerofile)) + assert_file.exist?(regular_file) + assert_equal(true, File.empty?(regular_file)) + assert_equal(content, File.read(zerofile)) + assert_raise(Errno::ENOENT) { File.exchange(regular_file, nofile) } + rescue NotImplementedError + end if POSIX + def test_umask prev = File.umask(0777) assert_equal(0777, File.umask)