From 5b1c20f2315cd7fc624ffd31abdcc03b409bfcb2 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 2 Jul 2016 17:21:54 +0300 Subject: Add cpfile() --- butl/fdstream | 51 ++++++++++++- butl/fdstream.cxx | 130 +++++++++++++++++++++++++++++++-- butl/fdstream.ixx | 25 +++++++ butl/filesystem | 50 +++++++++++++ butl/filesystem.cxx | 105 ++++++++++++++++++++++++-- butl/filesystem.ixx | 18 +++++ tests/buildfile | 4 +- tests/cpfile/buildfile | 7 ++ tests/cpfile/driver.cxx | 182 ++++++++++++++++++++++++++++++++++++++++++++++ tests/fdstream/buildfile | 7 ++ tests/fdstream/driver.cxx | 135 ++++++++++++++++++++++++++++++++++ 11 files changed, 700 insertions(+), 14 deletions(-) create mode 100644 butl/fdstream.ixx create mode 100644 tests/cpfile/buildfile create mode 100644 tests/cpfile/driver.cxx create mode 100644 tests/fdstream/buildfile create mode 100644 tests/fdstream/driver.cxx diff --git a/butl/fdstream b/butl/fdstream index 21df3f8..23cc58c 100644 --- a/butl/fdstream +++ b/butl/fdstream @@ -7,6 +7,10 @@ #include #include +#include // uint16_t + +#include +#include // 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 + #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 #ifndef _WIN32 -# include // open(), O_RDWR -# include // close(), read(), write() +# include // open(), O_* +# include // close(), read(), write() +# include // S_I* #else -# include // _close(), _read(), _write(), _setmode(), _sopen() -# include // _SH_DENYNO -# include // _fileno(), stdin, stdout, stderr -# include // _O_BINARY, _O_TEXT +# include // _close(), _read(), _write(), _setmode(), _sopen() +# include // _SH_DENYNO +# include // _fileno(), stdin, stdout, stderr +# include // _O_* +# include // S_I* #endif #include @@ -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 (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 ( + static_cast (x) & + static_cast (y)); + } + + inline fdopen_mode + operator|= (fdopen_mode& x, fdopen_mode y) + { + return x = static_cast ( + static_cast (x) | + static_cast (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 // struct dirent, *dir() # include // symlink(), link(), stat(), rmdir(), unlink() # include // stat -# include // stat(), lstat(), S_IS*, mkdir() +# include // stat(), lstat(), S_I*, mkdir(), chmod() #else # include -# include // _find*(), _unlink() +# include // _find*(), _unlink(), _chmod() # include // _mkdir(), _rmdir() # include // _stat -# include // _stat(), S_IF* - -# include +# include // _stat(), S_I* #endif #include // errno, E* @@ -25,6 +23,8 @@ #include // unique_ptr #include +#include + 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 (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 (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:: ~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 ( + static_cast (x) & + static_cast (y)); + } + + inline cpflags operator|= (cpflags& x, cpflags y) + { + return x = static_cast ( + static_cast (x) | + static_cast (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 +#include +#include +#include + +#include +#include + +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 +#include +#include + +#include +#include +#include + +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); +} -- cgit v1.1