aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2016-07-02 17:21:54 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2016-07-07 17:39:46 +0300
commit5b1c20f2315cd7fc624ffd31abdcc03b409bfcb2 (patch)
treec5c7b76dead92292b84586bda1b1256170195c48
parent15634965e8f0ab753898f5607ba11288556d1235 (diff)
Add cpfile()
-rw-r--r--butl/fdstream51
-rw-r--r--butl/fdstream.cxx130
-rw-r--r--butl/fdstream.ixx25
-rw-r--r--butl/filesystem50
-rw-r--r--butl/filesystem.cxx105
-rw-r--r--butl/filesystem.ixx18
-rw-r--r--tests/buildfile4
-rw-r--r--tests/cpfile/buildfile7
-rw-r--r--tests/cpfile/driver.cxx182
-rw-r--r--tests/fdstream/buildfile7
-rw-r--r--tests/fdstream/driver.cxx135
11 files changed, 700 insertions, 14 deletions
diff --git a/butl/fdstream b/butl/fdstream
index 21df3f8..23cc58c 100644
--- a/butl/fdstream
+++ b/butl/fdstream
@@ -7,6 +7,10 @@
#include <istream>
#include <ostream>
+#include <cstdint> // uint16_t
+
+#include <butl/path>
+#include <butl/filesystem> // permissions
namespace butl
{
@@ -73,7 +77,7 @@ namespace butl
private:
int fd_ = -1;
- char buf_[2048];
+ char buf_[4096];
};
// File descriptor translation mode. It has the same semantics as the
@@ -129,6 +133,49 @@ namespace butl
bool is_open () const {return buf_.is_open ();}
};
+ // File open flags.
+ //
+ enum class fdopen_mode: std::uint16_t
+ {
+ in = 0x01, // Open for reading.
+ out = 0x02, // Open for writing.
+ append = 0x04, // Seek to the end of file before each write.
+ truncate = 0x08, // Discard the file contents on open.
+ create = 0x10, // Create a file if not exists.
+ exclusive = 0x20, // Fail if the file exists and the create flag is set.
+ binary = 0x40, // Set binary translation mode.
+
+ none = 0 // Usefull when build the mode incrementally.
+ };
+
+ fdopen_mode operator& (fdopen_mode, fdopen_mode);
+ fdopen_mode operator| (fdopen_mode, fdopen_mode);
+ fdopen_mode operator&= (fdopen_mode&, fdopen_mode);
+ fdopen_mode operator|= (fdopen_mode&, fdopen_mode);
+
+ // Open a file returning the file descriptor on success and throwing
+ // std::system_error otherwise.
+ //
+ // The mode argument should have at least one of the in or out flags set.
+ // The append and truncate flags are meaningless in the absense of the out
+ // flag and are ignored without it. The exclusive flag is meaningless in the
+ // absense of the create flag and is ignored without it. Note also that if
+ // the exclusive flag is specified then a dangling symbolic link is treated
+ // as an existing file.
+ //
+ // The permissions argument is taken into account only if the file is
+ // created. Note also that permissions can be adjusted while being set in a
+ // way specific for the OS. On POSIX systems they are modified with the
+ // process' umask, so effective permissions are permissions & ~umask. On
+ // Windows permissions other than ru and wu are unlikelly to have effect.
+ //
+ int
+ fdopen (const path&,
+ fdopen_mode,
+ permissions = permissions::ru | permissions::wu |
+ permissions::rg | permissions::wg |
+ permissions::ro | permissions::wo);
+
// Set the translation mode for the file descriptor. Return the previous
// mode on success, throw std::system_error otherwise.
//
@@ -161,4 +208,6 @@ namespace butl
fdnull () noexcept;
}
+#include <butl/fdstream.ixx>
+
#endif // BUTL_FDSTREAM
diff --git a/butl/fdstream.cxx b/butl/fdstream.cxx
index 13becfa..e4d11ba 100644
--- a/butl/fdstream.cxx
+++ b/butl/fdstream.cxx
@@ -5,13 +5,15 @@
#include <butl/fdstream>
#ifndef _WIN32
-# include <fcntl.h> // open(), O_RDWR
-# include <unistd.h> // close(), read(), write()
+# include <fcntl.h> // open(), O_*
+# include <unistd.h> // close(), read(), write()
+# include <sys/stat.h> // S_I*
#else
-# include <io.h> // _close(), _read(), _write(), _setmode(), _sopen()
-# include <share.h> // _SH_DENYNO
-# include <stdio.h> // _fileno(), stdin, stdout, stderr
-# include <fcntl.h> // _O_BINARY, _O_TEXT
+# include <io.h> // _close(), _read(), _write(), _setmode(), _sopen()
+# include <share.h> // _SH_DENYNO
+# include <stdio.h> // _fileno(), stdin, stdout, stderr
+# include <fcntl.h> // _O_*
+# include <sys/stat.h> // S_I*
#endif
#include <system_error>
@@ -147,6 +149,122 @@ namespace butl
// Utility functions
//
+ int
+ fdopen (const path& f, fdopen_mode m, permissions p)
+ {
+ mode_t pf (S_IREAD | S_IWRITE | S_IEXEC);
+
+#ifdef S_IRWXG
+ pf |= S_IRWXG;
+#endif
+
+#ifdef S_IRWXO
+ pf |= S_IRWXO;
+#endif
+
+ pf &= static_cast<mode_t> (p);
+
+ // Return true if the open mode contains a specific flag.
+ //
+ auto mode = [m](fdopen_mode flag) -> bool {return (m & flag) == flag;};
+
+ int of (0);
+ bool in (mode (fdopen_mode::in));
+ bool out (mode (fdopen_mode::out));
+
+#ifndef _WIN32
+
+ if (in && out)
+ of |= O_RDWR;
+ else if (in)
+ of |= O_RDONLY;
+ else if (out)
+ of |= O_WRONLY;
+
+ if (out)
+ {
+ if (mode (fdopen_mode::append))
+ of |= O_APPEND;
+
+ if (mode (fdopen_mode::truncate))
+ of |= O_TRUNC;
+ }
+
+ if (mode (fdopen_mode::create))
+ {
+ of |= O_CREAT;
+
+ if (mode (fdopen_mode::exclusive))
+ of |= O_EXCL;
+ }
+
+#ifdef O_LARGEFILE
+ of |= O_LARGEFILE;
+#endif
+
+ int fd (open (f.string ().c_str (), of, pf));
+
+#else
+
+ if (in && out)
+ of |= _O_RDWR;
+ else if (in)
+ of |= _O_RDONLY;
+ else if (out)
+ of |= _O_WRONLY;
+
+ if (out)
+ {
+ if (mode (fdopen_mode::append))
+ of |= _O_APPEND;
+
+ if (mode (fdopen_mode::truncate))
+ of |= _O_TRUNC;
+ }
+
+ if (mode (fdopen_mode::create))
+ {
+ of |= _O_CREAT;
+
+ if (mode (fdopen_mode::exclusive))
+ of |= _O_EXCL;
+ }
+
+ of |= mode (fdopen_mode::binary) ? _O_BINARY : _O_TEXT;
+
+ // According to Microsoft _sopen() should not change the permissions of an
+ // existing file. Meanwhile it does if we pass them (reproduced on Windows
+ // XP, 7, and 8). And we must pass them if we have _O_CREATE. So we need
+ // to take care of preserving the permissions ourselves. Note that Wine's
+ // implementation of _sopen() works properly.
+ //
+ bool pass_perm (of & _O_CREAT);
+
+ if (pass_perm && file_exists (f))
+ {
+ // If the _O_CREAT flag is set then we need to clear it so that we can
+ // omit the permissions. But if the _O_EXCL flag is set as well we can't
+ // do that as fdopen() wouldn't fail as expected.
+ //
+ if (of & _O_EXCL)
+ throw system_error (EEXIST, system_category ());
+
+ of &= ~_O_CREAT;
+ pass_perm = false;
+ }
+
+ int fd (pass_perm
+ ? _sopen (f.string ().c_str (), of, _SH_DENYNO, pf)
+ : _sopen (f.string ().c_str (), of, _SH_DENYNO));
+
+#endif
+
+ if (fd == -1)
+ throw system_error (errno, system_category ());
+
+ return fd;
+ }
+
#ifndef _WIN32
bool
diff --git a/butl/fdstream.ixx b/butl/fdstream.ixx
new file mode 100644
index 0000000..a3ea83c
--- /dev/null
+++ b/butl/fdstream.ixx
@@ -0,0 +1,25 @@
+// file : butl/fdstream.ixx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+namespace butl
+{
+ inline fdopen_mode operator& (fdopen_mode x, fdopen_mode y) {return x &= y;}
+ inline fdopen_mode operator| (fdopen_mode x, fdopen_mode y) {return x |= y;}
+
+ inline fdopen_mode
+ operator&= (fdopen_mode& x, fdopen_mode y)
+ {
+ return x = static_cast<fdopen_mode> (
+ static_cast<std::uint16_t> (x) &
+ static_cast<std::uint16_t> (y));
+ }
+
+ inline fdopen_mode
+ operator|= (fdopen_mode& x, fdopen_mode y)
+ {
+ return x = static_cast<fdopen_mode> (
+ static_cast<std::uint16_t> (x) |
+ static_cast<std::uint16_t> (y));
+ }
+}
diff --git a/butl/filesystem b/butl/filesystem
index 780779f..7b23fc5 100644
--- a/butl/filesystem
+++ b/butl/filesystem
@@ -162,6 +162,47 @@ namespace butl
mkhardlink (target, link, true);
}
+ // File copy flags.
+ //
+ enum class cpflags: std::uint16_t
+ {
+ overwrite_content = 0x1,
+ overwrite_permissions = 0x2,
+
+ none = 0
+ };
+
+ cpflags operator& (cpflags, cpflags);
+ cpflags operator| (cpflags, cpflags);
+ cpflags operator&= (cpflags&, cpflags);
+ cpflags operator|= (cpflags&, cpflags);
+
+ // Copy a regular file, including its permissions. Throw std::system_error
+ // on failure. Fail if the destination file exists and the overwrite_content
+ // flag is not set. Leave permissions of an existing destination file intact
+ // unless the overwrite_permissions flag is set. Delete incomplete copies
+ // before throwing.
+ //
+ // Note that in case of overwriting, the existing destination file gets
+ // truncated (not deleted) prior to being overwritten. As a side-effect,
+ // hard link to the destination file will still reference the same file
+ // system node after the copy.
+ //
+ // Also note that if the overwrite_content flag is not set and the
+ // destination is a dangling symbolic link, then this function will still
+ // fail.
+ //
+ void
+ cpfile (const path& from, const path& to, cpflags = cpflags::none);
+
+ // Copy a regular file to an existing directory.
+ //
+ inline void
+ cpfile (const path& from, const dir_path& to, cpflags fl = cpflags::none)
+ {
+ cpfile (from, to / from.leaf (), fl);
+ }
+
// Return timestamp_nonexistent if the entry at the specified path
// does not exist or is not a path. All other errors are reported
// by throwing std::system_error. Note that this function resolves
@@ -196,9 +237,18 @@ namespace butl
permissions operator&= (permissions&, permissions);
permissions operator|= (permissions&, permissions);
+ // Get path permissions. Throw std::system_error on failure. Note that this
+ // function resolves symlinks.
+ //
permissions
path_permissions (const path&);
+ // Set path permissions. Throw std::system_error on failure. Note that this
+ // function resolves symlinks.
+ //
+ void
+ path_permissions (const path&, permissions);
+
// Directory entry iteration.
//
enum class entry_type
diff --git a/butl/filesystem.cxx b/butl/filesystem.cxx
index 0ff0e28..eab62dc 100644
--- a/butl/filesystem.cxx
+++ b/butl/filesystem.cxx
@@ -8,16 +8,14 @@
# include <dirent.h> // struct dirent, *dir()
# include <unistd.h> // symlink(), link(), stat(), rmdir(), unlink()
# include <sys/types.h> // stat
-# include <sys/stat.h> // stat(), lstat(), S_IS*, mkdir()
+# include <sys/stat.h> // stat(), lstat(), S_I*, mkdir(), chmod()
#else
# include <butl/win32-utility>
-# include <io.h> // _find*(), _unlink()
+# include <io.h> // _find*(), _unlink(), _chmod()
# include <direct.h> // _mkdir(), _rmdir()
# include <sys/types.h> // _stat
-# include <sys/stat.h> // _stat(), S_IF*
-
-# include <cassert>
+# include <sys/stat.h> // _stat(), S_I*
#endif
#include <errno.h> // errno, E*
@@ -25,6 +23,8 @@
#include <memory> // unique_ptr
#include <system_error>
+#include <butl/fdstream>
+
using namespace std;
#ifdef _MSC_VER // Unlikely to be fixed in newer versions.
@@ -223,6 +223,68 @@ namespace butl
}
#endif
+ void
+ cpfile (const path& from, const path& to, cpflags fl)
+ {
+ permissions perm (path_permissions (from));
+
+ // We do not enable exceptions to be thrown when badbit/failbit are set for
+ // the ifs stream nor check the bits manually down the road as there is no
+ // input functions being called for the stream. Input functions are called
+ // directly for the input stream buffer, which is our fdbuf that throws.
+ //
+ ifdstream ifs (fdopen (from, fdopen_mode::in | fdopen_mode::binary));
+
+ fdopen_mode om (fdopen_mode::out |
+ fdopen_mode::truncate |
+ fdopen_mode::create |
+ fdopen_mode::binary);
+
+ if ((fl & cpflags::overwrite_content) != cpflags::overwrite_content)
+ om |= fdopen_mode::exclusive;
+
+ // Create prior to the output file stream creation so that the file is
+ // removed after it is closed.
+ //
+ auto_rmfile rm;
+
+ ofdstream ofs (fdopen (to, om, perm));
+ rm = auto_rmfile (to);
+
+ // Setting badbit for the ofs stream is the only way to make sure the
+ // original std::system_error, that is thrown on the output operation
+ // failure, is retrown by the output stream's operator<<() and flush()
+ // calls (the latter is called from close()). Note that both of the
+ // functions behave as UnformattedOutputFunction.
+ //
+ ofs.exceptions (ofdstream::badbit);
+
+ // If the output operation ends up with the badbit set for a reason other
+ // than std::system_error being thrown (by fdbuf), then ofdstream::failure
+ // will be thrown instead. We need to convert it to std::system_error to
+ // comply with the cpfile() interface specification.
+ //
+ try
+ {
+ // Throws std::system_error on fdbuf read/write failures.
+ //
+ ofs << ifs.rdbuf ();
+
+ ifs.close ();
+ ofs.close (); // Throws std::system_error on flush failure.
+ }
+ catch (const ofdstream::failure& e)
+ {
+ throw system_error (EIO, system_category (), e.what ());
+ }
+
+ if ((fl & cpflags::overwrite_permissions) ==
+ cpflags::overwrite_permissions)
+ path_permissions (to, perm);
+
+ rm.cancel ();
+ }
+
// Figuring out whether we have the nanoseconds in struct stat. Some
// platforms (e.g., FreeBSD), may provide some "compatibility" #define's,
// so use the second argument to not end up with the same signatures.
@@ -255,8 +317,13 @@ namespace butl
timestamp
file_mtime (const path& p)
{
+#ifndef _WIN32
struct stat s;
if (stat (p.string ().c_str (), &s) != 0)
+#else
+ struct _stat s;
+ if (_stat (p.string ().c_str (), &s) != 0)
+#endif
{
if (errno == ENOENT || errno == ENOTDIR)
return timestamp_nonexistent;
@@ -274,8 +341,13 @@ namespace butl
permissions
path_permissions (const path& p)
{
+#ifndef _WIN32
struct stat s;
if (stat (p.string ().c_str (), &s) != 0)
+#else
+ struct _stat s;
+ if (_stat (p.string ().c_str (), &s) != 0)
+#endif
throw system_error (errno, system_category ());
// VC++ has no S_IRWXU defined. MINGW GCC <= 4.9 has no S_IRWXG, S_IRWXO
@@ -299,6 +371,29 @@ namespace butl
return static_cast<permissions> (s.st_mode & f);
}
+ void
+ path_permissions (const path& p, permissions f)
+ {
+ mode_t m (S_IREAD | S_IWRITE | S_IEXEC);
+
+#ifdef S_IRWXG
+ m |= S_IRWXG;
+#endif
+
+#ifdef S_IRWXO
+ m |= S_IRWXO;
+#endif
+
+ m &= static_cast<mode_t> (f);
+
+#ifndef _WIN32
+ if (chmod (p.string ().c_str (), m) == -1)
+#else
+ if (_chmod (p.string ().c_str (), m) == -1)
+#endif
+ throw system_error (errno, system_category ());
+ }
+
// dir_{entry,iterator}
//
#ifndef _WIN32
diff --git a/butl/filesystem.ixx b/butl/filesystem.ixx
index 037ab8b..8e81b59 100644
--- a/butl/filesystem.ixx
+++ b/butl/filesystem.ixx
@@ -46,6 +46,24 @@ namespace butl
inline auto_rm<dir_path>::
~auto_rm () {if (!path_.empty ()) try_rmdir_r (path_, true);}
+ // cpflags
+ //
+ inline cpflags operator& (cpflags x, cpflags y) {return x &= y;}
+ inline cpflags operator| (cpflags x, cpflags y) {return x |= y;}
+ inline cpflags operator&= (cpflags& x, cpflags y)
+ {
+ return x = static_cast<cpflags> (
+ static_cast<std::uint16_t> (x) &
+ static_cast<std::uint16_t> (y));
+ }
+
+ inline cpflags operator|= (cpflags& x, cpflags y)
+ {
+ return x = static_cast<cpflags> (
+ static_cast<std::uint16_t> (x) |
+ static_cast<std::uint16_t> (y));
+ }
+
// permissions
//
inline permissions operator& (permissions x, permissions y) {return x &= y;}
diff --git a/tests/buildfile b/tests/buildfile
index 311a290..8f20b9f 100644
--- a/tests/buildfile
+++ b/tests/buildfile
@@ -2,8 +2,8 @@
# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-d = base64/ dir-iterator/ link/ pager/ path/ prefix-map/ process/ sha256/ \
- timestamp/ triplet/
+d = base64/ cpfile/ dir-iterator/ fdstream/ link/ pager/ path/ prefix-map/ \
+ process/ sha256/ timestamp/ triplet/
.: $d
include $d
diff --git a/tests/cpfile/buildfile b/tests/cpfile/buildfile
new file mode 100644
index 0000000..7d02738
--- /dev/null
+++ b/tests/cpfile/buildfile
@@ -0,0 +1,7 @@
+# file : tests/cpfile/buildfile
+# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+exe{driver}: cxx{driver} ../../butl/lib{butl}
+
+include ../../butl/
diff --git a/tests/cpfile/driver.cxx b/tests/cpfile/driver.cxx
new file mode 100644
index 0000000..669ac26
--- /dev/null
+++ b/tests/cpfile/driver.cxx
@@ -0,0 +1,182 @@
+// file : tests/cpfile/driver.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <string>
+#include <cassert>
+#include <fstream>
+#include <system_error>
+
+#include <butl/path>
+#include <butl/filesystem>
+
+using namespace std;
+using namespace butl;
+
+static const char text1[] = "ABCDEF\nXYZ";
+static const char text2[] = "12345\nDEF";
+static const char text3[] = "XAB\r\n9";
+
+static string
+from_file (const path& f)
+{
+ ifstream ifs;
+ ifs.exceptions (fstream::badbit | fstream::failbit);
+ ifs.open (f.string (), ios::binary);
+
+ string s;
+ getline (ifs, s, '\0');
+ return s;
+}
+
+static void
+to_file (const path& f, const char* s)
+{
+ ofstream ofs;
+ ofs.exceptions (fstream::badbit | fstream::failbit);
+ ofs.open (f.string (), ios::binary);
+ ofs << s;
+}
+
+int
+main ()
+{
+ dir_path td (dir_path::temp_directory () / dir_path ("butl-cpfile"));
+
+ // Recreate the temporary directory (that possibly exists from the previous
+ // faulty run) for the test files. Delete the directory only if the test
+ // succeeds to simplify the failure research.
+ //
+ try_rmdir_r (td);
+ assert (try_mkdir (td) == mkdir_status::success);
+
+ path from (td / path ("from"));
+ to_file (from, text1);
+
+ permissions p (path_permissions (from));
+ path_permissions (from, permissions::ru | permissions::xu);
+
+ // Check that content and permissions of newly created destination file are
+ // the same as that ones of the source file.
+ //
+ path to (td / path ("to"));
+ cpfile (from, to, cpflags::none);
+ assert (from_file (to) == text1);
+ assert (path_permissions (to) == path_permissions (from));
+
+ // Check that permissions of an existent destination file stays intact if
+ // their overwrite is not requested.
+ //
+ path_permissions (to, p);
+ cpfile (from, to, cpflags::overwrite_content);
+ assert (from_file (to) == text1);
+ assert (path_permissions (to) == p);
+
+ // Check that permissions of an existent destination file get overwritten if
+ // requested.
+ //
+ cpfile (
+ from, to, cpflags::overwrite_content | cpflags::overwrite_permissions);
+
+ assert (from_file (to) == text1);
+ assert (path_permissions (to) == path_permissions (from));
+
+ path_permissions (to, p);
+ path_permissions (from, p);
+
+ try
+ {
+ cpfile (from, to, cpflags::none);
+ assert (false);
+ }
+ catch (const system_error&)
+ {
+ }
+
+ // Copy to the directory.
+ //
+ dir_path sd (td / dir_path ("sub"));
+ assert (try_mkdir (sd) == mkdir_status::success);
+
+ cpfile (from, sd, cpflags::none);
+
+ assert (from_file (sd / path ("from")) == text1);
+
+ // Check that a hard link to the destination file is preserved.
+ //
+ path hlink (td / path ("hlink"));
+ mkhardlink (to, hlink);
+ to_file (hlink, text1);
+
+ to_file (from, text2);
+ cpfile (from, to, cpflags::overwrite_content);
+
+ assert (from_file (hlink) == text2);
+
+#ifndef _WIN32
+
+ // Check that 'from' being a symbolic link is properly resolved.
+ //
+ path fslink (td / path ("fslink"));
+ mksymlink (from, fslink);
+
+ cpfile (fslink, to, cpflags::overwrite_content);
+
+ // Make sure 'to' is not a symbolic link to 'from' and from_file() just
+ // follows it.
+ //
+ assert (try_rmfile (from) == rmfile_status::success);
+ assert (from_file (to) == text2);
+
+ // Check that 'to' being a symbolic link is properly resolved.
+ //
+ path tslink (td / path ("tslink"));
+ mksymlink (to, tslink);
+
+ to_file (from, text3);
+ cpfile (from, tslink, cpflags::overwrite_content);
+ assert (from_file (to) == text3);
+
+ // Check that permissions are properly overwritten when 'to' is a symbolic
+ // link.
+ //
+ to_file (from, text1);
+ path_permissions (from, permissions::ru | permissions::xu);
+
+ cpfile (
+ from, tslink, cpflags::overwrite_content | cpflags::overwrite_permissions);
+
+ assert (from_file (to) == text1);
+ assert (path_permissions (to) == path_permissions (from));
+
+ path_permissions (to, p);
+ path_permissions (from, p);
+
+ // Check that no-overwrite file copy fails even if 'to' symlink points to
+ // non-existent file.
+ //
+ assert (try_rmfile (to) == rmfile_status::success);
+
+ try
+ {
+ cpfile (from, tslink, cpflags::none);
+ assert (false);
+ }
+ catch (const system_error&)
+ {
+ }
+
+ // Check that copy fail if 'from' symlink points to non-existent file.
+ //
+ try
+ {
+ cpfile (tslink, from, cpflags::none);
+ assert (false);
+ }
+ catch (const system_error&)
+ {
+ }
+#endif
+
+ rmdir_r (td);
+}
diff --git a/tests/fdstream/buildfile b/tests/fdstream/buildfile
new file mode 100644
index 0000000..bc4c6d2
--- /dev/null
+++ b/tests/fdstream/buildfile
@@ -0,0 +1,7 @@
+# file : tests/fdstream/buildfile
+# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+exe{driver}: cxx{driver} ../../butl/lib{butl}
+
+include ../../butl/
diff --git a/tests/fdstream/driver.cxx b/tests/fdstream/driver.cxx
new file mode 100644
index 0000000..3972473
--- /dev/null
+++ b/tests/fdstream/driver.cxx
@@ -0,0 +1,135 @@
+// file : tests/fdstream/driver.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <string>
+#include <cassert>
+#include <system_error>
+
+#include <butl/path>
+#include <butl/fdstream>
+#include <butl/filesystem>
+
+using namespace std;
+using namespace butl;
+
+static const string text1 ("ABCDEF\nXYZ");
+static const string text2 ("12"); // Keep shorter than text1.
+static const string text3 ("ABCDEF\r\nXYZ");
+
+static string
+from_file (const path& f, fdopen_mode m = fdopen_mode::none)
+{
+ ifdstream ifs (fdopen (f, m | fdopen_mode::in));
+ ifs.exceptions (ifdstream::badbit);
+
+ string s;
+ getline (ifs, s, '\0');
+ return s;
+}
+
+static void
+to_file (const path& f, const string& s, fdopen_mode m = fdopen_mode::none)
+{
+ ofdstream ofs (fdopen (f, m | fdopen_mode::out));
+ ofs.exceptions (ofdstream::badbit | ofdstream::failbit);
+ ofs << s;
+}
+
+int
+main ()
+{
+ dir_path td (dir_path::temp_directory () / dir_path ("butl-fdstream"));
+
+ // Recreate the temporary directory (that possibly exists from the previous
+ // faulty run) for the test files. Delete the directory only if the test
+ // succeeds to simplify the failure research.
+ //
+ try_rmdir_r (td);
+ assert (try_mkdir (td) == mkdir_status::success);
+
+ path f (td / path ("file"));
+
+ try
+ {
+ fdopen (f, fdopen_mode::out); // fdopen_mode::create is missed.
+ assert (false);
+ }
+ catch (const system_error&)
+ {
+ }
+
+ // Read from the newly created empty file.
+ //
+ assert (from_file (f, fdopen_mode::create) == "");
+ assert (try_rmfile (f) == rmfile_status::success);
+
+ to_file (f, text1, fdopen_mode::create);
+ assert (from_file (f) == text1);
+
+ // Read from the file opened in R/W mode.
+ //
+ assert (from_file (f, fdopen_mode::out) == text1);
+
+ try
+ {
+ // Fail to create if the file already exists.
+ //
+ fdopen (
+ f, fdopen_mode::out | fdopen_mode::create | fdopen_mode::exclusive);
+
+ assert (false);
+ }
+ catch (const system_error&)
+ {
+ }
+
+ // Write text2 over text1.
+ //
+ to_file (f, text2);
+ string s (text2);
+ s += string (text1, text2.size ());
+ assert (from_file (f) == s);
+
+ // Truncate before reading.
+ //
+ assert (from_file (f, fdopen_mode::out | fdopen_mode::truncate) == "");
+
+ // Append to the file.
+ //
+ to_file (f, text1, fdopen_mode::truncate);
+ to_file (f, text2, fdopen_mode::append);
+ assert (from_file (f) == text1 + text2);
+
+#ifndef _WIN32
+
+ // Fail for an existing symlink to unexistent file.
+ //
+ path link (td / path ("link"));
+ mksymlink (td / path ("unexistent"), link);
+
+ try
+ {
+ fdopen (
+ link, fdopen_mode::out | fdopen_mode::create | fdopen_mode::exclusive);
+
+ assert (false);
+ }
+ catch (const system_error&)
+ {
+ }
+
+#else
+
+ // Check translation modes.
+ //
+ to_file (f, text1, fdopen_mode::truncate);
+ assert (from_file (f, fdopen_mode::binary) == text3);
+
+ to_file (f, text3, fdopen_mode::truncate | fdopen_mode::binary);
+ assert (from_file (f) == text1);
+
+#endif
+
+ rmdir_r (td);
+}