patch 9.1.0465: missing filecopy() function

Problem:  missing filecopy() function
Solution: implement filecopy() Vim script function
          (Shougo Matsushita)

closes: #12346

Co-authored-by: zeertzjq <zeertzjq@outlook.com>
Signed-off-by: Shougo Matsushita <Shougo.Matsu@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 3028cf9..9720691 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -2007,6 +2007,8 @@
 			ret_void,	    f_feedkeys},
     {"file_readable",	1, 1, FEARG_1,	    arg1_string,	// obsolete
 			ret_number_bool,    f_filereadable},
+    {"filecopy",	2, 2, FEARG_1,	    arg2_string,
+			ret_number_bool,    f_filecopy},
     {"filereadable",	1, 1, FEARG_1,	    arg1_string,
 			ret_number_bool,    f_filereadable},
     {"filewritable",	1, 1, FEARG_1,	    arg1_string,
diff --git a/src/fileio.c b/src/fileio.c
index 07e05fc..e7f3332 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -3770,19 +3770,12 @@
     int
 vim_rename(char_u *from, char_u *to)
 {
-    int		fd_in;
-    int		fd_out;
     int		n;
-    char	*errmsg = NULL;
-    char	*buffer;
+    int		ret;
 #ifdef AMIGA
     BPTR	flock;
 #endif
     stat_T	st;
-    long	perm;
-#ifdef HAVE_ACL
-    vim_acl_T	acl;		// ACL from original file
-#endif
     int		use_tmp_file = FALSE;
 
     /*
@@ -3903,6 +3896,61 @@
     /*
      * Rename() failed, try copying the file.
      */
+    ret = vim_copyfile(from, to);
+    if (ret != OK)
+	return -1;
+
+    /*
+     * Remove copied original file
+     */
+    if (mch_stat((char *)from, &st) >= 0)
+	mch_remove(from);
+
+    return 0;
+}
+
+
+/*
+ * Create the new file with same permissions as the original.
+ * Return -1 for failure, 0 for success.
+ */
+    int
+vim_copyfile(char_u *from, char_u *to)
+{
+    int		fd_in;
+    int		fd_out;
+    int		n;
+    char	*errmsg = NULL;
+    char	*buffer;
+    long	perm;
+#ifdef HAVE_ACL
+    vim_acl_T	acl;		// ACL from original file
+#endif
+
+#ifdef HAVE_READLINK
+    int		ret;
+    int		len;
+    stat_T	st;
+    char	linkbuf[MAXPATHL + 1];
+
+    ret = mch_lstat((char *)from, &st);
+    if (ret >= 0 && S_ISLNK(st.st_mode))
+    {
+        ret = FAIL;
+
+	len = readlink((char *)from, linkbuf, MAXPATHL);
+	if (len > 0)
+	{
+	    linkbuf[len] = NUL;
+
+	    // Create link
+	    ret = symlink(linkbuf, (char *)to);
+	}
+
+	return ret == 0 ? OK : FAIL;
+    }
+#endif
+
     perm = mch_getperm(from);
 #ifdef HAVE_ACL
     // For systems that support ACL: get the ACL from the original file.
@@ -3914,7 +3962,7 @@
 #ifdef HAVE_ACL
 	mch_free_acl(acl);
 #endif
-	return -1;
+	return FAIL;
     }
 
     // Create the new file with same permissions as the original.
@@ -3926,7 +3974,7 @@
 #ifdef HAVE_ACL
 	mch_free_acl(acl);
 #endif
-	return -1;
+	return FAIL;
     }
 
     buffer = alloc(WRITEBUFSIZE);
@@ -3937,7 +3985,7 @@
 #ifdef HAVE_ACL
 	mch_free_acl(acl);
 #endif
-	return -1;
+	return FAIL;
     }
 
     while ((n = read_eintr(fd_in, buffer, WRITEBUFSIZE)) > 0)
@@ -3969,10 +4017,9 @@
     if (errmsg != NULL)
     {
 	semsg(errmsg, to);
-	return -1;
+	return FAIL;
     }
-    mch_remove(from);
-    return 0;
+    return OK;
 }
 
 static int already_warned = FALSE;
diff --git a/src/filepath.c b/src/filepath.c
index e68075a..9f68d7c 100644
--- a/src/filepath.c
+++ b/src/filepath.c
@@ -2649,6 +2649,31 @@
     rettv->v_type = VAR_STRING;
 }
 
+/*
+ * "filecopy()" function
+ */
+    void
+f_filecopy(typval_T *argvars, typval_T *rettv)
+{
+    char_u	*from;
+    stat_T	st;
+
+    rettv->vval.v_number = FALSE;
+
+    if (check_restricted() || check_secure()
+	|| check_for_string_arg(argvars, 0) == FAIL
+	|| check_for_string_arg(argvars, 1) == FAIL)
+	return;
+
+    from = tv_get_string(&argvars[0]);
+
+    if (mch_lstat((char *)from, &st) >= 0
+	&& (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)))
+	rettv->vval.v_number = vim_copyfile(
+	    tv_get_string(&argvars[0]),
+	    tv_get_string(&argvars[1])) == OK ? TRUE : FALSE;
+}
+
 #endif // FEAT_EVAL
 
 /*
diff --git a/src/proto/fileio.pro b/src/proto/fileio.pro
index 3f7b30d..8d97870 100644
--- a/src/proto/fileio.pro
+++ b/src/proto/fileio.pro
@@ -26,6 +26,7 @@
 char_u *buf_modname(int shortname, char_u *fname, char_u *ext, int prepend_dot);
 int vim_fgets(char_u *buf, int size, FILE *fp);
 int vim_rename(char_u *from, char_u *to);
+int vim_copyfile(char_u *from, char_u *to);
 int check_timestamps(int focus);
 int buf_check_timestamp(buf_T *buf, int focus);
 void buf_reload(buf_T *buf, int orig_mode, int reload_options);
diff --git a/src/proto/filepath.pro b/src/proto/filepath.pro
index 53fa4ec..46f51cb 100644
--- a/src/proto/filepath.pro
+++ b/src/proto/filepath.pro
@@ -6,6 +6,7 @@
 void f_delete(typval_T *argvars, typval_T *rettv);
 void f_executable(typval_T *argvars, typval_T *rettv);
 void f_exepath(typval_T *argvars, typval_T *rettv);
+void f_filecopy(typval_T *argvars, typval_T *rettv);
 void f_filereadable(typval_T *argvars, typval_T *rettv);
 void f_filewritable(typval_T *argvars, typval_T *rettv);
 void f_finddir(typval_T *argvars, typval_T *rettv);
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index a80b130..e31d2b5 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -143,6 +143,7 @@
 	test_file_perm \
 	test_file_size \
 	test_filechanged \
+	test_filecopy \
 	test_fileformat \
 	test_filetype \
 	test_filter_cmd \
@@ -404,6 +405,7 @@
 	test_expr.res \
 	test_file_size.res \
 	test_filechanged.res \
+	test_filecopy.res \
 	test_fileformat.res \
 	test_filetype.res \
 	test_filter_cmd.res \
diff --git a/src/testdir/test_filecopy.vim b/src/testdir/test_filecopy.vim
new file mode 100644
index 0000000..b526dce
--- /dev/null
+++ b/src/testdir/test_filecopy.vim
@@ -0,0 +1,72 @@
+" Test filecopy()
+
+source check.vim
+source shared.vim
+
+func Test_copy_file_to_file()
+  call writefile(['foo'], 'Xcopy1')
+
+  call assert_true(filecopy('Xcopy1', 'Xcopy2'))
+
+  call assert_equal(['foo'], readfile('Xcopy2'))
+
+  " When the destination file already exists, it should not be overwritten.
+  call writefile(['foo'], 'Xcopy1')
+  call writefile(['bar'], 'Xcopy2', 'D')
+  call assert_false(filecopy('Xcopy1', 'Xcopy2'))
+  call assert_equal(['bar'], readfile('Xcopy2'))
+
+  call delete('Xcopy2')
+  call delete('Xcopy1')
+endfunc
+
+func Test_copy_symbolic_link()
+  CheckUnix
+
+  call writefile(['text'], 'Xtestfile', 'D')
+  silent !ln -s -f Xtestfile Xtestlink
+
+  call assert_true(filecopy('Xtestlink', 'Xtestlink2'))
+  call assert_equal('link', getftype('Xtestlink2'))
+  call assert_equal(['text'], readfile('Xtestlink2'))
+
+  " When the destination file already exists, it should not be overwritten.
+  call assert_false(filecopy('Xtestlink', 'Xtestlink2'))
+
+  call delete('Xtestlink2')
+  call delete('Xtestlink')
+  call delete('Xtestfile')
+endfunc
+
+func Test_copy_dir_to_dir()
+  call mkdir('Xcopydir1')
+  call writefile(['foo'], 'Xcopydir1/Xfilecopy')
+  call mkdir('Xcopydir2')
+
+  " Directory copy is not supported
+  call assert_false(filecopy('Xcopydir1', 'Xcopydir2'))
+
+  call delete('Xcopydir2', 'rf')
+  call delete('Xcopydir1', 'rf')
+endfunc
+
+func Test_copy_fails()
+  CheckUnix
+
+  call writefile(['foo'], 'Xfilecopy', 'D')
+
+  " Can't copy into a non-existing directory.
+  call assert_false(filecopy('Xfilecopy', 'Xdoesnotexist/Xfilecopy'))
+
+  " Can't copy a non-existing file.
+  call assert_false(filecopy('Xdoesnotexist', 'Xfilecopy2'))
+  call assert_equal('', glob('Xfilecopy2'))
+
+  " Can't copy to en empty file name.
+  call assert_false(filecopy('Xfilecopy', ''))
+
+  call assert_fails('call filecopy("Xfilecopy", [])', 'E1174:')
+  call assert_fails('call filecopy(0z, "Xfilecopy")', 'E1174:')
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 4b1bcf1..c3d6199 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    465,
+/**/
     464,
 /**/
     463,