aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--butl/filesystem81
-rw-r--r--butl/filesystem.cxx148
-rw-r--r--tests/cpfile/driver.cxx2
-rw-r--r--tests/mventry/buildfile7
-rw-r--r--tests/mventry/driver.cxx49
-rw-r--r--tests/mventry/testscript245
6 files changed, 523 insertions, 9 deletions
diff --git a/butl/filesystem b/butl/filesystem
index 336233c..4e0395c 100644
--- a/butl/filesystem
+++ b/butl/filesystem
@@ -243,12 +243,87 @@ namespace butl
LIBBUTL_EXPORT void
cpfile (const path& from, const path& to, cpflags = cpflags::none);
- // Copy a regular file to an existing directory.
+ // Copy a regular file into (inside) an existing directory.
//
inline void
- cpfile (const path& from, const dir_path& to, cpflags fl = cpflags::none)
+ cpfile_into (const path& from,
+ const dir_path& into,
+ cpflags fl = cpflags::none)
{
- cpfile (from, to / from.leaf (), fl);
+ cpfile (from, into / from.leaf (), fl);
+ }
+
+ // Rename a filesystem entry (file, symlink, or directory). Throw
+ // std::system_error on failure.
+ //
+ // If the source path refers to a directory, then the destination path must
+ // either not exist, or refer to an empty directory. If the source path
+ // refers to an entry that is not a directory, then the destination path must
+ // not exist or not refer to a directory.
+ //
+ // If the source path refers to a symlink, then the link is renamed. If the
+ // destination path refers to a symlink, then the link will be overwritten.
+ //
+ // If the source and destination paths are on different file systems (or
+ // different drives on Windows) and the underlying OS does not support move
+ // for the source entry, then fail unless the source paths refers to a file
+ // or a file symlink. In this case fall back to copying the source file
+ // (content, permissions, access and modification times) and removing the
+ // source entry afterwards.
+ //
+ // Note that the operation is atomic only on POSIX, only if source and
+ // destination paths are on the same file system, and only if the
+ // overwrite_content flag is specified.
+ //
+ LIBBUTL_EXPORT void
+ mventry (const path& from,
+ const path& to,
+ cpflags = cpflags::overwrite_permissions);
+
+ // Move a filesystem entry into (inside) an existing directory.
+ //
+ inline void
+ mventry_into (const path& from,
+ const dir_path& into,
+ cpflags f = cpflags::overwrite_permissions)
+ {
+ mventry (from, into / from.leaf (), f);
+ }
+
+ // Raname file or file symlink.
+ //
+ inline void
+ mvfile (const path& from,
+ const path& to,
+ cpflags f = cpflags::overwrite_permissions)
+ {
+ mventry (from, to, f);
+ }
+
+ inline void
+ mvfile_into (const path& from,
+ const dir_path& into,
+ cpflags f = cpflags::overwrite_permissions)
+ {
+ mventry_into (from, into, f);
+ }
+
+ // Raname directory or directory symlink.
+ //
+ inline void
+ mvdir (const dir_path& from,
+ const dir_path& to,
+ cpflags f = cpflags::overwrite_permissions)
+ {
+ mventry (from, to, f);
+ }
+
+ inline void
+ mvdir_into (const path& from,
+ const dir_path& into,
+ cpflags f = cpflags::overwrite_permissions)
+ {
+ mventry_into (from, into, f);
}
// Return timestamp_nonexistent if the entry at the specified path
diff --git a/butl/filesystem.cxx b/butl/filesystem.cxx
index bd03d7a..e881417 100644
--- a/butl/filesystem.cxx
+++ b/butl/filesystem.cxx
@@ -5,8 +5,10 @@
#include <butl/filesystem>
#ifndef _WIN32
+# include <stdio.h> // rename()
# include <dirent.h> // struct dirent, *dir()
# include <unistd.h> // symlink(), link(), stat(), rmdir(), unlink()
+# include <sys/time.h> // utimes()
# include <sys/types.h> // stat
# include <sys/stat.h> // stat(), lstat(), S_I*, mkdir(), chmod()
#else
@@ -386,28 +388,164 @@ namespace butl
//
template <typename S>
inline constexpr auto
- nsec (const S* s, bool) -> decltype(s->st_mtim.tv_nsec)
+ mnsec (const S* s, bool) -> decltype(s->st_mtim.tv_nsec)
{
return s->st_mtim.tv_nsec; // POSIX (GNU/Linux, Solaris).
}
template <typename S>
inline constexpr auto
- nsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec)
+ mnsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec)
{
return s->st_mtimespec.tv_nsec; // *BSD, MacOS.
}
template <typename S>
inline constexpr auto
- nsec (const S* s, float) -> decltype(s->st_mtime_n)
+ mnsec (const S* s, float) -> decltype(s->st_mtime_n)
{
return s->st_mtime_n; // AIX 5.2 and later.
}
template <typename S>
inline constexpr int
- nsec (...) {return 0;}
+ mnsec (...) {return 0;}
+
+ template <typename S>
+ inline constexpr auto
+ ansec (const S* s, bool) -> decltype(s->st_atim.tv_nsec)
+ {
+ return s->st_atim.tv_nsec; // POSIX (GNU/Linux, Solaris).
+ }
+
+ template <typename S>
+ inline constexpr auto
+ ansec (const S* s, int) -> decltype(s->st_atimespec.tv_nsec)
+ {
+ return s->st_atimespec.tv_nsec; // *BSD, MacOS.
+ }
+
+ template <typename S>
+ inline constexpr auto
+ ansec (const S* s, float) -> decltype(s->st_atime_n)
+ {
+ return s->st_atime_n; // AIX 5.2 and later.
+ }
+
+ template <typename S>
+ inline constexpr int
+ ansec (...) {return 0;}
+
+ void
+ mventry (const path& from, const path& to, cpflags fl)
+ {
+ assert ((fl & cpflags::overwrite_permissions) ==
+ cpflags::overwrite_permissions);
+
+ bool ovr ((fl & cpflags::overwrite_content) == cpflags::overwrite_content);
+
+ const char* f (from.string ().c_str ());
+ const char* t (to.string ().c_str ());
+
+#ifndef _WIN32
+
+ if (!ovr && path_entry (to).first)
+ throw system_error (EEXIST, system_category ());
+
+ if (::rename (f, t) == 0) // POSIX implementation.
+ return;
+
+ // If source and destination paths are on different file systems we need to
+ // move the file ourselves.
+ //
+ if (errno != EXDEV)
+ throw system_error (errno, system_category ());
+
+ // Note that cpfile() follows symlinks, so we need to remove destination if
+ // exists.
+ //
+ try_rmfile (to);
+
+ // Note that permissions are copied unconditionally to a new file.
+ //
+ cpfile (from, to, cpflags::none);
+
+ // Copy file access and modification times.
+ //
+ struct stat s;
+ if (stat (f, &s) != 0)
+ throw system_error (errno, system_category ());
+
+ timeval times[2];
+ times[0].tv_sec = s.st_atime;
+ times[0].tv_usec = ansec<struct stat> (&s, true) / 1000;
+ times[1].tv_sec = s.st_mtime;
+ times[1].tv_usec = mnsec<struct stat> (&s, true) / 1000;
+
+ if (utimes (t, times) != 0)
+ throw system_error (errno, system_category ());
+
+ // Finally, remove the source file.
+ //
+ try_rmfile (from);
+
+#else
+
+ // While ::rename() is present on Windows, it is not POSIX but ISO C
+ // implementation, that doesn't fit our needs well.
+ //
+ auto te (path_entry (to));
+
+ // Note that it would be nicer to just pass standard error codes to
+ // system_error ctors below, but their error descriptions horrifies for
+ // msvcrt. Actually they just don't match the semantics. The resulted error
+ // description is also not ideal (see below).
+ //
+ if (!ovr && te.first)
+ throw system_error (EEXIST, system_category (), "file exists");
+
+ bool td (te.first && te.second == entry_type::directory);
+
+ auto fe (path_entry (from));
+ bool fd (fe.first && fe.second == entry_type::directory);
+
+ // If source and destination filesystem entries exist, they both must be
+ // either directories or not directories.
+ //
+ if (fe.first && te.first && fd != td)
+ throw system_error (EIO, system_category (), "not a directory");
+
+ DWORD mfl (fd ? 0 : (MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING));
+
+ if (MoveFileExA (f, t, mfl))
+ return;
+
+ // If the destination already exists, then MoveFileExA() succeeds only if
+ // it is a regular file or a symlink. Lets also support an empty directory
+ // special case to comply with POSIX. If the destination is an empty
+ // directory we will just remove it and retry the move operation.
+ //
+ // Note that under Wine we endup with ERROR_ACCESS_DENIED error code in
+ // that case, and with ERROR_ALREADY_EXISTS when run natively.
+ //
+ DWORD ec (GetLastError ());
+ if ((ec == ERROR_ALREADY_EXISTS || ec == ERROR_ACCESS_DENIED) && td &&
+ try_rmdir (path_cast<dir_path> (to)) != rmdir_status::not_empty &&
+ MoveFileExA (f, t, mfl))
+ return;
+
+ // @@ The exception description will look like:
+ //
+ // file not found. : Access denied
+ //
+ // Probably need to consider such discriptions for sanitizing in
+ // operator<<(ostream,exception).
+ //
+ string e (win32::error_msg (ec));
+ throw system_error (EIO, system_category (), e);
+
+#endif
+ }
timestamp
file_mtime (const char* p)
@@ -429,7 +567,7 @@ namespace butl
return S_ISREG (s.st_mode)
? system_clock::from_time_t (s.st_mtime) +
chrono::duration_cast<duration> (
- chrono::nanoseconds (nsec<struct stat> (&s, true)))
+ chrono::nanoseconds (mnsec<struct stat> (&s, true)))
: timestamp_nonexistent;
}
diff --git a/tests/cpfile/driver.cxx b/tests/cpfile/driver.cxx
index 732b364..5b8f366 100644
--- a/tests/cpfile/driver.cxx
+++ b/tests/cpfile/driver.cxx
@@ -110,7 +110,7 @@ main ()
dir_path sd (td / dir_path ("sub"));
assert (try_mkdir (sd) == mkdir_status::success);
- cpfile (from, sd, cpflags::none);
+ cpfile_into (from, sd, cpflags::none);
assert (from_file (sd / path ("from")) == text1);
diff --git a/tests/mventry/buildfile b/tests/mventry/buildfile
new file mode 100644
index 0000000..868a2f6
--- /dev/null
+++ b/tests/mventry/buildfile
@@ -0,0 +1,7 @@
+# file : tests/mventry/buildfile
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+exe{driver}: cxx{driver} ../../butl/lib{butl} test{testscript}
+
+include ../../butl/
diff --git a/tests/mventry/driver.cxx b/tests/mventry/driver.cxx
new file mode 100644
index 0000000..cf6099c
--- /dev/null
+++ b/tests/mventry/driver.cxx
@@ -0,0 +1,49 @@
+// file : tests/mventry/driver.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <iostream>
+#include <system_error>
+
+#include <butl/path>
+#include <butl/utility> // operator<<(ostream, exception)
+#include <butl/filesystem>
+
+using namespace std;
+using namespace butl;
+
+// Usage: argv[0] <old-path> <new-path>
+//
+// Rename a file, directory or symlink or move it to the specified directory.
+// For the later case the destination path must have a trailing directory
+// separator. If succeed then exits with the zero code, otherwise prints the
+// error descriptions and exits with the one code.
+//
+int
+main (int argc, const char* argv[])
+try
+{
+ assert (argc == 3);
+
+ path from (argv[1]);
+ path to (argv[2]);
+
+ cpflags fl (cpflags::overwrite_permissions | cpflags::overwrite_content);
+
+ if (to.to_directory ())
+ mventry_into (from, path_cast<dir_path> (move (to)), fl);
+ else
+ mventry (from, to, fl);
+
+ return 0;
+}
+catch (const invalid_path& e)
+{
+ cerr << e << ": " << e.path << endl;
+ return 1;
+}
+catch (const system_error& e)
+{
+ cerr << e << endl;
+ return 1;
+}
diff --git a/tests/mventry/testscript b/tests/mventry/testscript
new file mode 100644
index 0000000..0a0aaa0
--- /dev/null
+++ b/tests/mventry/testscript
@@ -0,0 +1,245 @@
+# file : tests/mventry/testscript
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+: file
+:
+{
+ : non-existing
+ :
+ $* a b 2>- == 1
+
+ : over
+ :
+ {
+ : non-existing
+ :
+ echo 'foo' >=a &!a;
+ $* a b &b;
+ cat b >'foo';
+ test -f a == 1
+
+ : existing-file
+ :
+ echo 'foo' >=a &!a;
+ echo 'bar' >=b;
+ $* a b;
+ cat b >'foo';
+ test -f a == 1
+
+ : existing-dir
+ :
+ echo 'foo' >=a;
+ mkdir b;
+ $* a b 2>- == 1
+ }
+
+ : to-dir
+ :
+ echo 'foo' >=a &!a;
+ mkdir b;
+ $* a b/ &b/a;
+ cat b/a >'foo';
+ test -f a == 1
+}
+
+: dir
+:
+{
+ : over
+ {
+ : non-existing
+ :
+ mkdir -p a/b &!a/b/ &!a/;
+ echo 'foo' >=a/c &!a/c;
+ $* a b &b/ &b/c &b/b/;
+ cat b/c >'foo';
+ test -d b/b;
+ test -d a == 1
+
+ : empty-dir
+ :
+ mkdir -p a/b &!a/b/ &!a/;
+ echo 'foo' >=a/c &!a/c;
+ mkdir b;
+ $* a b &b/c &b/b/;
+ cat b/c >'foo';
+ test -d b/b;
+ test -d a == 1
+
+ : non-empty-dir
+ :
+ mkdir -p a/b;
+ mkdir -p b/d;
+ $* a b 2>- == 1
+
+ : existing-file
+ :
+ mkdir a;
+ touch b;
+ $* a b 2>- == 1
+ }
+
+ : to-dir
+ :
+ mkdir a &!a/;
+ mkdir b;
+ $* a b/ &b/a/;
+ test -d b/a;
+ test -f a == 1
+}
+
+: symlink
+:
+: If we are not cross-testing let's test renaming symlynks from and over. On
+: Windows that involves mklink command usability test. If we fail to create a
+: trial link (say because we are running non-administrative console), then the
+: test group will be silently skipped.
+:
+if ($test.target == $build.host)
+{
+ +if ($cxx.target.class != 'windows')
+ lns = ln -s a b
+ else
+ echo 'yes' >=a
+ if cmd /C 'mklink b a' >- 2>- &?b && cat b >'yes'
+ lns = cmd /C 'mklink b a' >-
+ end
+ end
+
+ if! $empty($lns)
+ {
+ : file
+ :
+ {
+ : from
+ :
+ : Make sure that if source is a symlink it refers the same target after
+ : rename.
+ :
+ echo 'foo' >=a;
+ $lns;
+ $* b c &c;
+ test -f a;
+ test -f b == 1;
+ echo 'bar' >=a;
+ cat c >'bar'
+
+ : to
+ :
+ : Make sure that if destination is a symlink it is get overwritten and it's
+ : target stays intact.
+ :
+ echo 'foo' >=a;
+ $lns &b;
+ echo 'bar' >=c &!c;
+ $* c b;
+ cat a >'foo';
+ test -f c == 1;
+ echo 'baz' >=a;
+ cat b >'bar'
+
+ : over-existing-dir
+ :
+ echo 'foo' >=a;
+ $lns &b;
+ mkdir c;
+ $* b c 2>- == 1
+ }
+
+ : dir
+ :
+ {
+ : from
+ :
+ : Make sure that if source is a symlink it refers the same target after
+ : rename.
+ :
+ mkdir -p a;
+ $lns;
+ $* b c &c;
+ touch a/b;
+ test -f c/b;
+ test -d b == 1
+
+ : to
+ :
+ : Make sure that if destination is a symlink it is get overwritten and
+ : it's target stays intact.
+ :
+ mkdir -p a;
+ $lns;
+ echo 'foo' >=c &!c;
+ $* c b &b;
+ cat b >'foo';
+ test -d a;
+ test -f c == 1
+
+ : over-existing-dir
+ :
+ mkdir a;
+ $lns &b;
+ mkdir c;
+ $* b c 2>- == 1
+ }
+ }
+}
+
+: different-fs
+:
+: Note that nested tests may fail for cross-testing as the directory path will
+: unlikelly be usable on both host (build2 driver) and target (test driver)
+: platforms.
+:
+if! $empty($config.libbutl.test.rename.dir)
+{
+ wd = $config.libbutl.test.rename.dir/libbutl-rename
+ +rm -r -f $wd
+ +mkdir $wd
+
+ : file
+ :
+ {
+ : over-non-existing
+ :
+ {
+ wd = "$wd/$@";
+ mkdir -p $wd;
+ b = $wd/b;
+
+ echo 'foo' >=a &!a;
+ $* a $b;
+ cat $b >'foo';
+ test -f a == 1
+ }
+
+ : over-file
+ :
+ {
+ wd = "$wd/$@";
+ mkdir -p $wd;
+ b = $wd/b;
+
+ touch $b;
+ echo 'foo' >=a &!a;
+ $* a $b;
+ cat $b >'foo';
+ test -f a == 1
+ }
+ }
+
+ : dir
+ :
+ : Test that renaming directory to different fs/drive expectedly fails.
+ :
+ {
+ wd = "$wd/$@";
+ mkdir -p $wd;
+ b = $wd/b;
+
+ mkdir a;
+ $* a $b 2>- == 1
+ }
+
+ -rm -r -f $wd
+}