From dcccba655fe848564e961b3f285ce3a82d3ac73a Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 7 Mar 2020 14:07:28 +0300 Subject: Add more support for symlinks on Windows See mksymlink() for details of the symlinks support on Windows. --- libbutl/buildfile | 2 +- libbutl/fdstream.cxx | 33 +- libbutl/filesystem.cxx | 1095 ++++++++++++++++++++++++----------------- libbutl/filesystem.mxx | 72 +-- libbutl/process.cxx | 43 -- libbutl/win32-utility.cxx | 26 +- libbutl/win32-utility.hxx | 81 +++ libbutl/win32-utility.ixx | 32 ++ tests/cpfile/driver.cxx | 134 ++--- tests/dir-iterator/testscript | 100 ++-- tests/fdstream/driver.cxx | 38 +- tests/link/driver.cxx | 93 +++- tests/mventry/testscript | 51 +- tests/path-entry/driver.cxx | 212 +++++++- tests/path-entry/testscript | 175 ++++++- tests/wildcard/testscript | 52 +- 16 files changed, 1535 insertions(+), 704 deletions(-) create mode 100644 libbutl/win32-utility.ixx diff --git a/libbutl/buildfile b/libbutl/buildfile index 26fb6b5..2d40031 100644 --- a/libbutl/buildfile +++ b/libbutl/buildfile @@ -25,7 +25,7 @@ windows = ($tclass == 'windows') # Exclude these from compilation on non-Windows targets. # -lib{butl}: {hxx cxx}{win32-utility}: include = $windows +lib{butl}: {hxx ixx cxx}{win32-utility}: include = $windows # Our C-files are included into sha256.cxx (sha256c.c) and timestamp.cxx # (strptime.c timelocal.h timelocal.c), so treat them as files exclude from diff --git a/libbutl/fdstream.cxx b/libbutl/fdstream.cxx index b58240c..54f3e9e 100644 --- a/libbutl/fdstream.cxx +++ b/libbutl/fdstream.cxx @@ -1091,19 +1091,34 @@ namespace butl // to take care of preserving the permissions ourselves. Note that Wine's // implementation of _sopen() works properly. // + // Also note that _sopen() creates a dangling symlink target if _O_EXCL is + // set. Thus, we need to prevent such a creation manually. + // bool pass_perm (of & _O_CREAT); - if (pass_perm && file_exists (path (f))) + if (pass_perm) { - // 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_generic_ios_failure (EEXIST); + if (file_exists (f)) // Follows symlink. + { + // 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_generic_ios_failure (EEXIST); - of &= ~_O_CREAT; - pass_perm = false; + of &= ~_O_CREAT; + pass_perm = false; + } + else if (of & _O_EXCL) + { + pair pe (path_entry (f)); // Doesn't follow symlink. + + // Fail for a dangling symlink. + // + if (pe.first && pe.second.type == entry_type::symlink) + throw_generic_ios_failure (EEXIST); + } } // Make sure the file descriptor is not inheritable by default. diff --git a/libbutl/filesystem.cxx b/libbutl/filesystem.cxx index 1fec18b..d453006 100644 --- a/libbutl/filesystem.cxx +++ b/libbutl/filesystem.cxx @@ -95,7 +95,7 @@ namespace butl bool dir_exists (const char* p, bool ie) { - auto pe (path_entry (p, true, ie)); + auto pe (path_entry (p, true /* follow_symlinks */, ie)); return pe.first && pe.second.type == entry_type::directory; } @@ -128,6 +128,147 @@ namespace butl return make_pair (true, entry_stat {t, static_cast (s.st_size)}); } + permissions + path_permissions (const path& p) + { + struct stat s; + if (stat (p.string ().c_str (), &s) != 0) + throw_generic_error (errno); + + return static_cast (s.st_mode & + (S_IRWXU | S_IRWXG | S_IRWXO)); + } + + void + path_permissions (const path& p, permissions f) + { + if (chmod (p.string ().c_str (), static_cast (f)) == -1) + throw_generic_error (errno); + } + + // 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. + // + template + static inline constexpr auto + mnsec (const S* s, bool) -> decltype(s->st_mtim.tv_nsec) + { + return s->st_mtim.tv_nsec; // POSIX (GNU/Linux, Solaris). + } + + template + static inline constexpr auto + mnsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec) + { + return s->st_mtimespec.tv_nsec; // *BSD, MacOS. + } + + template + static inline constexpr auto + mnsec (const S* s, float) -> decltype(s->st_mtime_n) + { + return s->st_mtime_n; // AIX 5.2 and later. + } + + // Things are not going to end up well with only seconds resolution so + // let's make it a compile error. + // + // template + // static inline constexpr int + // mnsec (...) {return 0;} + + template + static 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 + static inline constexpr auto + ansec (const S* s, int) -> decltype(s->st_atimespec.tv_nsec) + { + return s->st_atimespec.tv_nsec; // *BSD, MacOS. + } + + template + static inline constexpr auto + ansec (const S* s, float) -> decltype(s->st_atime_n) + { + return s->st_atime_n; // AIX 5.2 and later. + } + + // template + // static inline constexpr int + // ansec (...) {return 0;} + + // Return the modification and access times of a regular file or directory. + // + static entry_time + entry_tm (const char* p, bool dir) + { + struct stat s; + if (stat (p, &s) != 0) + { + if (errno == ENOENT || errno == ENOTDIR) + return {timestamp_nonexistent, timestamp_nonexistent}; + else + throw_generic_error (errno); + } + + if (dir ? !S_ISDIR (s.st_mode) : !S_ISREG (s.st_mode)) + return {timestamp_nonexistent, timestamp_nonexistent}; + + auto tm = [] (time_t sec, auto nsec) -> timestamp + { + return system_clock::from_time_t (sec) + + chrono::duration_cast (chrono::nanoseconds (nsec)); + }; + + return {tm (s.st_mtime, mnsec (&s, true)), + tm (s.st_atime, ansec (&s, true))}; + } + + // Set the modification and access times for a regular file or directory. + // + static void + entry_tm (const char* p, const entry_time& t, bool dir) + { + struct stat s; + if (stat (p, &s) != 0) + throw_generic_error (errno); + + // If the entry is of the wrong type, then let's pretend that it doesn't + // exists. In other words, the entry of the required type doesn't exist. + // + if (dir ? !S_ISDIR (s.st_mode) : !S_ISREG (s.st_mode)) + return throw_generic_error (ENOENT); + + // Note: timeval has the microsecond resolution. + // + auto tm = [] (timestamp t, time_t sec, auto nsec) -> timeval + { + using usec_type = decltype (timeval::tv_usec); + + if (t == timestamp_nonexistent) + return {sec, static_cast (nsec / 1000)}; + + uint64_t usec (chrono::duration_cast ( + t.time_since_epoch ()).count ()); + + return {static_cast (usec / 1000000), // Seconds. + static_cast (usec % 1000000)}; // Microseconds. + }; + + timeval times[2]; + times[0] = tm (t.access, s.st_atime, ansec (&s, true)); + times[1] = tm (t.modification, s.st_mtime, mnsec (&s, true)); + + if (utimes (p, times) != 0) + throw_generic_error (errno); + } + #else static inline bool @@ -142,53 +283,143 @@ namespace butl } static inline bool + readonly (DWORD a) noexcept + { + return (a & FILE_ATTRIBUTE_READONLY) != 0; + } + + static inline bool + reparse_point (DWORD a) noexcept + { + return (a & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + } + + // Note: returns true for junctions. + // + static inline bool + directory (DWORD a) noexcept + { + return (a & FILE_ATTRIBUTE_DIRECTORY) != 0; + } + + static inline bool junction (DWORD a) noexcept { - return a != INVALID_FILE_ATTRIBUTES && - (a & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && - (a & FILE_ATTRIBUTE_DIRECTORY) != 0; + return reparse_point (a) && directory (a); } static inline bool - junction (const path& p) noexcept + junction (const char* p) noexcept { - return junction (GetFileAttributesA (p.string ().c_str ())); + DWORD a (GetFileAttributesA (p)); + return a != INVALID_FILE_ATTRIBUTES && junction (a); } - // Return true if the junction exists and is referencing an existing - // directory. Assume that the path references a junction. Underlying OS - // errors are reported by throwing std::system_error, unless ignore_error - // is true. + // Open a filesystem entry for reading and optionally writing its + // meta-information and return the entry handle and meta-information if the + // path refers to an existing entry and nullhandle otherwise. Follow reparse + // points. // - static bool - junction_target_exists (const char* p, bool ignore_error) + // Note that normally to update an entry meta-information you need the + // current info, unless you rewrite it completely (attributes, modification + // time, etc). + // + static inline pair + entry_info_handle (const char* p, bool write, bool ie = false) { - HANDLE h (CreateFile (p, - FILE_READ_ATTRIBUTES, - FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS, - NULL)); + using namespace win32; - if (h == INVALID_HANDLE_VALUE) + // Open the entry for reading/writing its meta-information. Follow reparse + // points. + // + // Note that the FILE_READ_ATTRIBUTES flag is not required. + // + auto_handle h ( + CreateFile (p, + write ? FILE_WRITE_ATTRIBUTES : 0, + 0, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, // Required for a directory. + NULL)); + + if (h == nullhandle) { DWORD ec; - - if (ignore_error || error_file_not_found (ec = GetLastError ())) - return false; + if (ie || error_file_not_found (ec = GetLastError ())) + return make_pair (nullhandle, BY_HANDLE_FILE_INFORMATION ()); throw_system_error (ec); } - CloseHandle (h); - return true; + BY_HANDLE_FILE_INFORMATION r; + if (!GetFileInformationByHandle (h.get (), &r)) + { + if (ie) + return make_pair (nullhandle, BY_HANDLE_FILE_INFORMATION ()); + + throw_system_error (GetLastError ()); + } + + return make_pair (move (h), r); } - static inline bool - junction_target_exists (const path& p, bool ignore_error) + static inline pair + entry_info_handle (const path& p, bool write, bool ie = false) + { + return entry_info_handle (p.string ().c_str (), write, ie); + } + + // Return a flag indicating whether the path is to an existing filesystem + // entry and its meta-information if so. Follow reparse points. + // + static inline pair + path_entry_info (const char* p, bool ie = false) { - return junction_target_exists (p.string ().c_str (), ignore_error); + using namespace win32; + + pair hi ( + entry_info_handle (p, false /* write */, ie)); + + if (hi.first == nullhandle) + return make_pair (false, BY_HANDLE_FILE_INFORMATION ()); + + if (!ie) + hi.first.close (); // Checks for error. + + return make_pair (true, hi.second); + } + + static inline pair + path_entry_info (const path& p, bool ie = false) + { + return path_entry_info (p.string ().c_str (), ie); + } + + // As above but return entry_stat. + // + static inline pair + path_entry_stat (const char* p, bool ie) + { + pair pi (path_entry_info (p, ie)); + + if (!pi.first) + return make_pair (false, entry_stat {entry_type::unknown, 0}); + + if (directory (pi.second.dwFileAttributes)) + return make_pair (true, entry_stat {entry_type::directory, 0}); + else + return make_pair ( + true, + entry_stat {entry_type::regular, + ((uint64_t (pi.second.nFileSizeHigh) << 32) | + pi.second.nFileSizeLow)}); + } + + static inline pair + path_entry_stat (const path& p, bool ie) + { + return path_entry_stat (p.string ().c_str (), ie); } pair @@ -208,49 +439,196 @@ namespace butl p = d.c_str (); } - // Note that VC's implementations of _stat64() follows junctions and fails - // for dangling ones. MinGW GCC's implementation returns the information - // about the junction itself. That's why we handle junctions specially, - // not relying on _stat64(). + // Get the path entry attributes and bail out if it doesn't exist. Note: + // doesn't follow reparse points. // DWORD a (GetFileAttributesA (p)); - if (a == INVALID_FILE_ATTRIBUTES) // Presumably not exists. - return make_pair (false, entry_stat {entry_type::unknown, 0}); + if (a == INVALID_FILE_ATTRIBUTES) + { + DWORD ec; + if (ie || error_file_not_found (ec = GetLastError ())) + return make_pair (false, entry_stat {entry_type::unknown, 0}); + + throw_system_error (ec); + } - if (junction (a)) + if (reparse_point (a)) { if (!fl) return make_pair (true, entry_stat {entry_type::symlink, 0}); - return junction_target_exists (p, ie) - ? make_pair (true, entry_stat {entry_type::directory, 0}) - : make_pair (false, entry_stat {entry_type::unknown, 0}); + // Fall through to stat the reparse point target. + // + } + else + { + if (directory (a)) + return make_pair (true, entry_stat {entry_type::directory, 0}); + + // Fall through to obtain the regular file size. + // } - entry_type et (entry_type::unknown); - struct __stat64 s; // For 64-bit size. + // Note that previously we used _stat64() to get the entry information but + // this doesn't work well for MinGW GCC where _stat64() returns the + // information about the reparse point itself and strangely ends up with + // ENOENT for symlink's target directory immediate sub-entries (but not + // for the more nested sub-entries). + // + return path_entry_stat (p, ie); + } + + permissions + path_permissions (const path& p) + { + pair pi (path_entry_info (p)); + + if (!pi.first) + throw_generic_error (ENOENT); + + // On Windows a filesystem entry is always readable. Also there is no + // notion of group/other permissions at OS level, so we extrapolate user + // permissions to group/other permissions (as the _stat() function does). + // + permissions r (permissions::ru | permissions::rg | permissions::ro); + + if (!readonly (pi.second.dwFileAttributes)) + r |= permissions::wu | permissions::wg | permissions::wo; + + return r; + } + + static inline FILETIME + to_filetime (timestamp t) + { + // Time in FILETIME is in 100 nanosecond "ticks" since "Windows epoch" + // (1601-01-01T00:00:00Z). To convert "UNIX epoch" + // (1970-01-01T00:00:00Z) to it we need to add 11644473600 seconds. + // + uint64_t ticks (chrono::duration_cast ( + t.time_since_epoch ()).count ()); + + ticks /= 100; // Now in 100 nanosecond "ticks". + ticks += 11644473600ULL * 10000000; // Now in "Windows epoch". + + FILETIME r; + r.dwHighDateTime = (ticks >> 32) & 0xFFFFFFFF; + r.dwLowDateTime = ticks & 0xFFFFFFFF; + return r; + } + + void + path_permissions (const path& p, permissions f) + { + using namespace win32; + + pair hi ( + entry_info_handle (p, true /* write */)); - if (_stat64 (p, &s) != 0) + if (hi.first == nullhandle) + throw_generic_error (ENOENT); + + const BY_HANDLE_FILE_INFORMATION& fi (hi.second); + DWORD attrs (fi.dwFileAttributes); + + if ((f & permissions::wu) != permissions::none) + attrs &= ~FILE_ATTRIBUTE_READONLY; + else + attrs |= FILE_ATTRIBUTE_READONLY; + + if (attrs != fi.dwFileAttributes) { - if (errno == ENOENT || errno == ENOTDIR || ie) - return make_pair (false, entry_stat {et, 0}); - else - throw_generic_error (errno); + auto tm = [] (const FILETIME& t) + { + LARGE_INTEGER r; // Is a union. + r.LowPart = t.dwLowDateTime; + r.HighPart = t.dwHighDateTime; + return r; + }; + + FILE_BASIC_INFO bi {tm (fi.ftCreationTime), + tm (fi.ftLastAccessTime), + tm (fi.ftLastWriteTime), + tm (to_filetime (system_clock::now ())), + attrs}; + + if (!SetFileInformationByHandle (hi.first.get (), + FileBasicInfo, + &bi, + sizeof (bi))) + throw_system_error (GetLastError ()); } - // Note that we currently support only directory symlinks (see mksymlink() - // for details). + hi.first.close (); // Checks for error. + } + + // Return the modification and access times of a regular file or directory. + // + static entry_time + entry_tm (const char* p, bool dir) + { + pair pi (path_entry_info (p)); + + // If the entry is of the wrong type, then let's pretend that it doesn't + // exists. // - if (S_ISREG (s.st_mode)) - et = entry_type::regular; - else if (S_ISDIR (s.st_mode)) - et = entry_type::directory; + if (!pi.first || directory (pi.second.dwFileAttributes) != dir) + return {timestamp_nonexistent, timestamp_nonexistent}; + + auto tm = [] (const FILETIME& t) -> timestamp + { + // Time in FILETIME is in 100 nanosecond "ticks" since "Windows epoch" + // (1601-01-01T00:00:00Z). To convert it to "UNIX epoch" + // (1970-01-01T00:00:00Z) we need to subtract 11644473600 seconds. + // + uint64_t nsec ((static_cast (t.dwHighDateTime) << 32) | + t.dwLowDateTime); + + nsec -= 11644473600ULL * 10000000; // Now in UNIX epoch. + nsec *= 100; // Now in nanoseconds. + + return timestamp ( + chrono::duration_cast (chrono::nanoseconds (nsec))); + }; + + return {tm (pi.second.ftLastWriteTime), tm (pi.second.ftLastAccessTime)}; + } + + // Set the modification and access times for a regular file or directory. + // + static void + entry_tm (const char* p, const entry_time& t, bool dir) + { + using namespace win32; + + pair hi ( + entry_info_handle (p, true /* write */)); + + // If the entry is of the wrong type, then let's pretend that it doesn't + // exists. // - //else if (S_ISLNK (s.st_mode)) - // et = entry_type::symlink; + if (hi.first == nullhandle || + directory (hi.second.dwFileAttributes) != dir) + return throw_generic_error (ENOENT); + + auto tm = [] (timestamp t, FILETIME& ft) -> const FILETIME* + { + if (t == timestamp_nonexistent) + return NULL; + + ft = to_filetime (t); + return &ft; + }; - return make_pair (true, - entry_stat {et, static_cast (s.st_size)}); + FILETIME at; + FILETIME mt; + if (!SetFileTime (hi.first.get (), + NULL /* lpCreationTime */, + tm (t.access, at), + tm (t.modification, mt))) + throw_system_error (GetLastError ()); + + hi.first.close (); // Checks for error. } #endif @@ -270,7 +648,9 @@ namespace butl #ifndef _WIN32 if (utime (p.string ().c_str (), nullptr) == -1) #else - if (_utime (p.string ().c_str (), nullptr) == -1) + // Note: follows reparse points. + // + if (_utime (p.string ().c_str (), nullptr) == -1) #endif throw_generic_error (errno); @@ -332,12 +712,50 @@ namespace butl try_rmdir (const dir_path& p, bool ignore_error) { rmdir_status r (rmdir_status::success); + const char* d (p.string ().c_str ()); #ifndef _WIN32 - if (rmdir (p.string ().c_str ()) != 0) + int rr (rmdir (d)); #else - if (_rmdir (p.string ().c_str ()) != 0) + // Note that we should only remove regular directories but not junctions. + // However, _rmdir() removes both and thus we need to check the entry type + // ourselves prior to removal. + // + DWORD a (GetFileAttributesA (d)); + bool va (a != INVALID_FILE_ATTRIBUTES); + + // Let's also check for non-directory not to end up with not very specific + // EINVAL. + // + if (va && (!directory (a) || reparse_point (a))) + throw_generic_error (ENOTDIR); + + int rr (_rmdir (d)); + + // On Windows a directory with the read-only attribute can not be deleted. + // This can be the reason if the deletion fails with the 'permission + // denied' error code. In such a case we just reset the attribute and + // repeat the attempt. If the attempt fails, then we try to restore the + // attribute. + // + if (rr != 0 && errno == EACCES) + { + if (va && + readonly (a) && + SetFileAttributes (d, a & ~FILE_ATTRIBUTE_READONLY)) + { + rr = _rmdir (d); + + // Restoring the attribute is unlikely to fail since we managed to + // reset it earlier. + // + if (rr != 0) + SetFileAttributes (d, a); + } + } #endif + + if (rr != 0) { if (errno == ENOENT) r = rmdir_status::not_exist; @@ -405,7 +823,7 @@ namespace butl // attempt. If the attempt fails, then we try to restore the attribute. // // Yet another reason for the 'permission denied' failure can be a - // directory symlink. + // junction. // // And also there are some unknown reasons for the 'permission denied' // failure (see mventry() for details). If that's the case, we will keep @@ -425,18 +843,20 @@ namespace butl DWORD a (GetFileAttributesA (f)); if (a != INVALID_FILE_ATTRIBUTES) { - bool readonly ((a & FILE_ATTRIBUTE_READONLY) != 0); + bool ro (readonly (a)); - // Note that we support only directory symlinks on Windows. + // Note that the reparse point can be a junction which we need to + // remove as a directory. // - bool symlink (junction (a)); + bool jn (junction (a)); - if (readonly || symlink) + if (ro || jn) { - bool restore (readonly && - SetFileAttributes (f, a & ~FILE_ATTRIBUTE_READONLY)); + bool restore (ro && + SetFileAttributes (f, + a & ~FILE_ATTRIBUTE_READONLY)); - ur = symlink ? _rmdir (f) : _unlink (f); + ur = jn ? _rmdir (f) : _unlink (f); // Restoring the attribute is unlikely to fail since we managed to // reset it earlier. @@ -475,12 +895,6 @@ namespace butl throw_generic_error (errno); } - rmfile_status - try_rmsymlink (const path& link, bool, bool ie) - { - return try_rmfile (link, ie); - } - void mkhardlink (const path& target, const path& link, bool) { @@ -493,9 +907,57 @@ namespace butl void mksymlink (const path& target, const path& link, bool dir) { + using namespace win32; + if (!dir) + { + // Try to create a regular symbolic link without elevated privileges by + // passing the new SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag. + // The flag is new and it may not be defined at compile-time so we pass + // its literal value. + // + // We may also be running on an earlier version of Windows (before 10 + // build 14972) that doesn't support it. The natural way to handle that + // would have been to check the build of Windows that we are running on + // but that turns out to not be that easy (requires a deprecated API, + // specific executable manifest setup, a dance on one leg, and who knows + // what else). + // + // So instead we are going to just make the call and if the result is + // the invalid argument error, assume that the flag is not recognized. + // Except that this error can also be returned if the link path or the + // target path are invalid. So if we get this error, we also stat the + // two paths to make sure we don't get the same error for them. + // + if (CreateSymbolicLinkA (link.string ().c_str (), + target.string ().c_str (), + 0x2)) + return; + + // Note that ERROR_INVALID_PARAMETER means that either the function + // doesn't recognize the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + // flag (that happens on elder systems) or the target or link paths are + // invalid. Thus, we additionally check the paths to distinguish the + // cases. + // + if (GetLastError () == ERROR_INVALID_PARAMETER) + { + auto invalid = [] (const path& p) + { + return GetFileAttributesA (p.string ().c_str ()) == + INVALID_FILE_ATTRIBUTES && + GetLastError () == ERROR_INVALID_PARAMETER; + }; + + if (invalid (target) || invalid (link)) + throw_generic_error (EINVAL); + } + throw_generic_error (ENOSYS, "file symlinks not supported"); + } + // Create as a junction. + // dir_path ld (path_cast (link)); mkdir_status rs (try_mkdir (ld)); @@ -511,30 +973,18 @@ namespace butl // auto_rmdir rm (ld); - HANDLE h (CreateFile (link.string ().c_str (), - GENERIC_WRITE, - 0, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | - FILE_FLAG_OPEN_REPARSE_POINT, - NULL)); + auto_handle h (CreateFile (link.string ().c_str (), + GENERIC_WRITE, + 0, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | + FILE_FLAG_OPEN_REPARSE_POINT, + NULL)); - if (h == INVALID_HANDLE_VALUE) + if (h == nullhandle) throw_system_error (GetLastError ()); - unique_ptr deleter ( - &h, - [] (HANDLE* h) - { - bool r (CloseHandle (*h)); - - // The valid file handle that has no IO operations being performed on - // it should close successfully, unless something is severely damaged. - // - assert (r); - }); - // We will fill the uninitialized members later. // struct @@ -547,18 +997,17 @@ namespace butl // Mount point reparse buffer. // - WORD substitute_name_offset = 0; + WORD substitute_name_offset; WORD substitute_name_length; WORD print_name_offset; - WORD print_name_length = 0; + WORD print_name_length; - // Reserve space for two NULL characters (for the names above). + // Reserve space for NULL character-terminated path and print name. // - wchar_t path_buffer[MAX_PATH + 2]; + wchar_t path_buffer[2 * MAX_PATH + 2]; } rb; - // Make the target path absolute, decorate it and convert to a - // wide-character string. + // Make the target path absolute and decorate it. // path atd (target); @@ -574,43 +1023,62 @@ namespace butl throw_generic_error (EINVAL); } - string td ("\\??\\" + atd.string () + "\\"); - const char* s (td.c_str ()); - - // Zero-initialize the conversion state (and disambiguate with the - // function declaration). + // Convert a path from the character string to the wide-character string + // representation and return the resulting string length in characters + // (first) and in bytes (second). Append the trailing NULL character to + // the resulting string but exclude it from the length. // - mbstate_t state ((mbstate_t ())); - size_t n (mbsrtowcs (rb.path_buffer, &s, MAX_PATH + 1, &state)); + auto conv = [] (const string& from, wchar_t* to) + { + const char* s (from.c_str ()); - if (n == static_cast (-1)) - throw_generic_error (errno); + // Zero-initialize the conversion state (and disambiguate with the + // function declaration). + // + mbstate_t state ((mbstate_t ())); + size_t n (mbsrtowcs (to, &s, MAX_PATH + 1, &state)); + + if (n == static_cast (-1)) + throw_generic_error (errno); + + if (s != NULL) // Not enough space in the destination buffer. + throw_generic_error (ENAMETOOLONG); + + return make_pair (n, static_cast (n * sizeof (wchar_t))); + }; + + // Convert the junction target path (\??\C:\...\). + // + pair np (conv ("\\??\\" + atd.string () + "\\", + rb.path_buffer)); - if (s != NULL) // Not enough space in the destination buffer. - throw_generic_error (ENAMETOOLONG); + // Convert the junction target print name (C:\...). + // + pair nn (conv (atd.string (), + rb.path_buffer + np.first + 1)); // Fill the rest of the structure and setup the reparse point. // - // The path length not including NULL character, in bytes. + // The path offset and length, in bytes. // - WORD nb (static_cast (n) * sizeof (wchar_t)); - rb.substitute_name_length = nb; + rb.substitute_name_offset = 0; + rb.substitute_name_length = np.second; - // The print name offset, in bytes. + // The print name offset and length, in bytes. // - rb.print_name_offset = nb + sizeof (wchar_t); - rb.path_buffer[n + 1] = L'\0'; // Terminate the (empty) print name. + rb.print_name_offset = np.second + sizeof (wchar_t); + rb.print_name_length = nn.second; // The mount point reparse buffer size. // rb.reparse_data_length = - 4 * sizeof (WORD) + // Size of *_name_* fields. - nb + sizeof (wchar_t) + // Path length, in bytes. - sizeof (wchar_t); // Print (empty) name length, in bytes. + 4 * sizeof (WORD) + // Size of *_name_* fields. + np.second + sizeof (wchar_t) + // Path length, in bytes. + nn.second + sizeof (wchar_t); // Print name length, in bytes. DWORD r; if (!DeviceIoControl ( - h, + h.get (), FSCTL_SET_REPARSE_POINT, &rb, sizeof (DWORD) + 2 * sizeof (WORD) + // Size of the header. @@ -618,19 +1086,12 @@ namespace butl NULL, // reparse buffer. 0, &r, - NULL)) - throw_system_error (GetLastError ()); - - rm.cancel (); - } - - rmfile_status - try_rmsymlink (const path& link, bool dir, bool ie) - { - if (!dir) - throw_generic_error (ENOSYS, "file symlinks not supported"); + NULL)) + throw_system_error (GetLastError ()); - return try_rmfile (link, ie); + h.close (); // Checks for error. + + rm.cancel (); } void @@ -672,6 +1133,12 @@ namespace butl } #endif + rmfile_status + try_rmsymlink (const path& link, bool, bool ie) + { + return try_rmfile (link, ie); + } + entry_type mkanylink (const path& target, const path& link, bool copy, bool rel) { @@ -814,63 +1281,6 @@ namespace butl 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. - // - template - static inline constexpr auto - mnsec (const S* s, bool) -> decltype(s->st_mtim.tv_nsec) - { - return s->st_mtim.tv_nsec; // POSIX (GNU/Linux, Solaris). - } - - template - static inline constexpr auto - mnsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec) - { - return s->st_mtimespec.tv_nsec; // *BSD, MacOS. - } - - template - static inline constexpr auto - mnsec (const S* s, float) -> decltype(s->st_mtime_n) - { - return s->st_mtime_n; // AIX 5.2 and later. - } - - // Things are not going to end up well with only seconds resolution so - // let's make it a compile error. - // - // template - // static inline constexpr int - // mnsec (...) {return 0;} - - template - static 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 - static inline constexpr auto - ansec (const S* s, int) -> decltype(s->st_atimespec.tv_nsec) - { - return s->st_atimespec.tv_nsec; // *BSD, MacOS. - } - - template - static inline constexpr auto - ansec (const S* s, float) -> decltype(s->st_atime_n) - { - return s->st_atime_n; // AIX 5.2 and later. - } - - // template - // static inline constexpr int - // ansec (...) {return 0;} - void mventry (const path& from, const path& to, cpflags fl) { @@ -957,20 +1367,35 @@ namespace butl ec = GetLastError (); // 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. + // if it is a regular file or a symlink but not a junction, failing with + // ERROR_ALREADY_EXISTS (ERROR_ACCESS_DENIED under Wine) for a regular + // directory and ERROR_ACCESS_DENIED for a junction. Thus, we remove a + // junction 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. + // 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. // - if ((ec == ERROR_ALREADY_EXISTS || ec == ERROR_ACCESS_DENIED) && td && - try_rmdir (path_cast (to)) != rmdir_status::not_empty) + if (ec == ERROR_ALREADY_EXISTS || ec == ERROR_ACCESS_DENIED) { - if (MoveFileExA (f, t, mfl)) - return; + bool retry (false); + + if (te.first && te.second.type == entry_type::symlink && junction (t)) + { + try_rmsymlink (to); + retry = true; + } + else if (td) + retry = try_rmdir (path_cast (to)) != + rmdir_status::not_empty; + + if (retry) + { + if (MoveFileExA (f, t, mfl)) + return; - ec = GetLastError (); + ec = GetLastError (); + } } if (ec != ERROR_SHARING_VIOLATION) @@ -982,73 +1407,6 @@ namespace butl #endif } - // Return the modification and access times of a regular file or directory. - // - static entry_time - entry_tm (const char* p, bool dir) - { -#ifndef _WIN32 - - struct stat s; - if (stat (p, &s) != 0) - { - if (errno == ENOENT || errno == ENOTDIR) - return {timestamp_nonexistent, timestamp_nonexistent}; - else - throw_generic_error (errno); - } - - if (dir ? !S_ISDIR (s.st_mode) : !S_ISREG (s.st_mode)) - return {timestamp_nonexistent, timestamp_nonexistent}; - - auto tm = [] (time_t sec, auto nsec) -> timestamp - { - return system_clock::from_time_t (sec) + - chrono::duration_cast (chrono::nanoseconds (nsec)); - }; - - return {tm (s.st_mtime, mnsec (&s, true)), - tm (s.st_atime, ansec (&s, true))}; - -#else - - WIN32_FILE_ATTRIBUTE_DATA s; - - if (!GetFileAttributesExA (p, GetFileExInfoStandard, &s)) - { - DWORD ec (GetLastError ()); - - if (error_file_not_found (ec)) - return {timestamp_nonexistent, timestamp_nonexistent}; - - throw_system_error (ec); - } - - if ((s.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != - (dir ? FILE_ATTRIBUTE_DIRECTORY : 0)) - return {timestamp_nonexistent, timestamp_nonexistent}; - - auto tm = [] (const FILETIME& t) -> timestamp - { - // Time in FILETIME is in 100 nanosecond "ticks" since "Windows epoch" - // (1601-01-01T00:00:00Z). To convert it to "UNIX epoch" - // (1970-01-01T00:00:00Z) we need to subtract 11644473600 seconds. - // - uint64_t nsec ((static_cast (t.dwHighDateTime) << 32) | - t.dwLowDateTime); - - nsec -= 11644473600ULL * 10000000; // Now in UNIX epoch. - nsec *= 100; // Now in nanoseconds. - - return timestamp ( - chrono::duration_cast (chrono::nanoseconds (nsec))); - }; - - return {tm (s.ftLastWriteTime), tm (s.ftLastAccessTime)}; - -#endif - } - entry_time file_time (const char* p) { @@ -1061,110 +1419,6 @@ namespace butl return entry_tm (p, true); } - // Set the modification and access times for a regular file or directory. - // - static void - entry_tm (const char* p, const entry_time& t, bool dir) - { -#ifndef _WIN32 - - struct stat s; - if (stat (p, &s) != 0) - throw_generic_error (errno); - - // If the entry is of the wrong type, then let's pretend that it doesn't - // exists. In other words, the entry of the required type doesn't exist. - // - if (dir ? !S_ISDIR (s.st_mode) : !S_ISREG (s.st_mode)) - return throw_generic_error (ENOENT); - - // Note: timeval has the microsecond resolution. - // - auto tm = [] (timestamp t, time_t sec, auto nsec) -> timeval - { - using usec_type = decltype (timeval::tv_usec); - - if (t == timestamp_nonexistent) - return {sec, static_cast (nsec / 1000)}; - - uint64_t usec (chrono::duration_cast ( - t.time_since_epoch ()).count ()); - - return {static_cast (usec / 1000000), // Seconds. - static_cast (usec % 1000000)}; // Microseconds. - }; - - timeval times[2]; - times[0] = tm (t.access, s.st_atime, ansec (&s, true)); - times[1] = tm (t.modification, s.st_mtime, mnsec (&s, true)); - - if (utimes (p, times) != 0) - throw_generic_error (errno); - -#else - - DWORD attr (GetFileAttributesA (p)); - - if (attr == INVALID_FILE_ATTRIBUTES) - throw_system_error (GetLastError ()); - - // If the entry is of the wrong type, then let's pretend that it doesn't - // exists. - // - if ((attr & FILE_ATTRIBUTE_DIRECTORY) != - (dir ? FILE_ATTRIBUTE_DIRECTORY : 0)) - return throw_generic_error (ENOENT); - - HANDLE h (CreateFile (p, - FILE_WRITE_ATTRIBUTES, - FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, - OPEN_EXISTING, - dir ? FILE_FLAG_BACKUP_SEMANTICS : 0, - NULL)); - - if (h == INVALID_HANDLE_VALUE) - throw_system_error (GetLastError ()); - - unique_ptr deleter ( - &h, [] (HANDLE* h) - { - bool r (CloseHandle (*h)); - - // The valid file handle that has no IO operations being performed on - // it should close successfully, unless something is severely damaged. - // - assert (r); - }); - - auto tm = [] (timestamp t, FILETIME& ft) -> const FILETIME* - { - if (t == timestamp_nonexistent) - return NULL; - - // Time in FILETIME is in 100 nanosecond "ticks" since "Windows epoch" - // (1601-01-01T00:00:00Z). To convert "UNIX epoch" - // (1970-01-01T00:00:00Z) to it we need to add 11644473600 seconds. - // - uint64_t ticks (chrono::duration_cast ( - t.time_since_epoch ()).count ()); - - ticks /= 100; // Now in 100 nanosecond "ticks". - ticks += 11644473600ULL * 10000000; // Now in "Windows epoch". - - ft.dwHighDateTime = (ticks >> 32) & 0xFFFFFFFF; - ft.dwLowDateTime = ticks & 0xFFFFFFFF; - return &ft; - }; - - FILETIME at; - FILETIME mt; - if (!SetFileTime (h, NULL, tm (t.access, at), tm (t.modification, mt))) - throw_system_error (GetLastError ()); - -#endif - } - void file_time (const char* p, const entry_time& t) { @@ -1177,62 +1431,6 @@ namespace butl entry_tm (p, t, true); } - 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_generic_error (errno); - - // VC++ has no S_IRWXU defined. MINGW GCC <= 4.9 has no S_IRWXG, S_IRWXO - // defined. - // - // We could extrapolate user permissions to group/other permissions if - // S_IRWXG/S_IRWXO are undefined. That is, we could consider their absence - // as meaning that the platform does not distinguish between permissions - // for different kinds of users. Let's wait for a use-case first. - // - mode_t f (S_IREAD | S_IWRITE | S_IEXEC); - -#ifdef S_IRWXG - f |= S_IRWXG; -#endif - -#ifdef S_IRWXO - f |= S_IRWXO; -#endif - - 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_generic_error (errno); - } - // dir_{entry,iterator} // #ifndef _WIN32 @@ -1278,11 +1476,11 @@ namespace butl } entry_type dir_entry:: - type (bool link) const + type (bool follow_symlinks) const { path_type p (b_ / p_); struct stat s; - if ((link + if ((follow_symlinks ? stat (p.string ().c_str (), &s) : lstat (p.string ().c_str (), &s)) != 0) throw_generic_error (errno); @@ -1439,10 +1637,10 @@ namespace butl } entry_type dir_entry:: - type (bool link) const + type (bool follow_symlinks) const { path_type p (base () / path ()); - pair e (path_entry (p, link)); + pair e (path_entry (p, follow_symlinks)); if (!e.first) throw_generic_error (ENOENT); @@ -1486,6 +1684,8 @@ namespace butl void dir_iterator:: next () { + using namespace win32; + for (;;) { bool r; @@ -1522,22 +1722,35 @@ namespace butl e_.p_ = move (p); - // An entry with the _A_SUBDIR attribute can also be a junction. + // Note that the entry can be a reparse point regardless of the + // _A_SUBDIR attribute presence and so its type detection always + // requires to additionally query the entry information. Thus, we + // evaluate its type lazily. // - e_.t_ = (fi.attrib & _A_SUBDIR) == 0 ? entry_type::regular : - junction (e_.base () / e_.path ()) ? entry_type::symlink : - entry_type::directory; + e_.t_ = entry_type::unknown; e_.lt_ = entry_type::unknown; - // If requested, we ignore dangling symlinks, skipping ones with - // non-existing or inaccessible targets. + // If requested, we ignore dangling symlinks and junctions, skipping + // ones with non-existing or inaccessible targets. // - if (ignore_dangling_ && - e_.ltype () == entry_type::symlink && - !junction_target_exists (e_.base () / e_.path (), - true /* ignore_error */)) - continue; + // Note that ltype() queries the entry information and so can throw. + // + if (ignore_dangling_ && e_.ltype () == entry_type::symlink) + { + // Query the target info. + // + pair te ( + path_entry_stat (e_.base () / e_.path (), + true /* ignore_error */)); + + if (!te.first) + continue; + + // While at it, set the target type. + // + e_.lt_ = te.second.type; + } } else if (errno == ENOENT) { diff --git a/libbutl/filesystem.mxx b/libbutl/filesystem.mxx index b3b0409..e83c666 100644 --- a/libbutl/filesystem.mxx +++ b/libbutl/filesystem.mxx @@ -115,9 +115,9 @@ LIBBUTL_MODEXPORT namespace butl }; // Return a flag indicating if the path is to an existing filesystem entry - // and its type if so. Note that by default this function doesn't follow - // symlinks. Underlying OS errors are reported by throwing std::system_error, - // unless ignore_error is true. + // and its info if so. Note that by default this function doesn't follow + // symlinks. Underlying OS errors are reported by throwing + // std::system_error, unless ignore_error is true. // LIBBUTL_SYMEXPORT std::pair path_entry (const char*, @@ -192,9 +192,9 @@ LIBBUTL_MODEXPORT namespace butl LIBBUTL_SYMEXPORT void rmdir_r (const dir_path&, bool dir = true, bool ignore_error = false); - // Try to remove the file (or symlinks) returning not_exist if - // it does not exist. Unless ignore_error is true, all other - // errors are reported by throwing std::system_error. + // Try to remove the file (or symlink) returning not_exist if it does not + // exist. Unless ignore_error is true, all other errors are reported by + // throwing std::system_error. // // Note that if it is known that the path refers to a symlink, then usage of // try_rmsymlink() function must be preferred, as a more efficient one. @@ -241,25 +241,41 @@ LIBBUTL_MODEXPORT namespace butl // Create a symbolic link to a file (default) or directory (third argument // is true). Throw std::system_error on failures. // - // Note that on Windows file symlinks are currently not supported. Directory - // symlinks are supported via the junction mechanism that doesn't require - // a process to have administrative privileges. This choice, however, - // introduces the following restrictions: - // - // - The relative target path is completed against the current directory and - // is normalized. - // - // - The target directory must exist. If it doesn't exists at the moment of - // a symlink creation, then mksymlink() call will fail. If the target is - // deleted at a later stage, then the filesystem API functions may fail - // when encounter such a symlink. This includes try_rmsymlink(). - // - // - Dangling symlinks are not visible when iterating over a directory with - // dir_iterator. As a result, a directory that contains such symlinks can - // not be recursively deleted. - // - // @@ Note that the above restrictions seems to be Wine-specific (as of - // 4.0). + // Note that on Windows symlinks are supported partially: + // + // - File symlinks are implemented via the Windows symlink mechanism and may + // only be created on Windows 10 Build 14972 and above with either the + // Developer Mode enabled or if the process runs in the elevated command + // prompt. + // + // - Directory symlinks are implemented via the Windows junction mechanism + // that doesn't require a process to have administrative privileges and so + // a junction can be created regardless of the Windows version and mode. + // Note that junctions, in contrast to symlinks, may only store target + // absolute paths. Thus, when create a directory symlink we complete its + // target path against the current directory (unless it is already + // absolute) and normalize. + // + // (@@ TODO: At some point we may want to support creating directory + // symlinks if possible and falling back to junctions otherwise. One + // potential issue here is with relative target paths which the caller + // cannot rely on staying relative. Plus there is the below bug.) + // + // - Functions other than mksymlink() fully support symlinks, considering + // the Windows file symlinks (file-type reparse points referring to files) + // as regular file symlinks and the Windows directory symlinks (file-type + // reparse points referring to directories) and junctions (directory-type + // reparse points referring to directories) as directory symlinks. The + // known issues are: + // + // - path_entry() call that follows a symlink (but not a junction) with a + // directory target may throw std::system_error due to the underlying + // CreateFile() function call failing with the ERROR_ACCESS_DENIED + // error. This appears to be a bug that has been noticed only on Windows + // with the Developer Mode enabled. + // + // Also note that symlinks are currently not supported properly on Wine due + // to some differences in the underlying API behavior. // LIBBUTL_SYMEXPORT void mksymlink (const path& target, const path& link, bool dir = false); @@ -629,7 +645,7 @@ LIBBUTL_MODEXPORT namespace butl private: entry_type - type (bool link) const; + type (bool follow_symlinks) const; private: friend class dir_iterator; @@ -656,10 +672,6 @@ LIBBUTL_MODEXPORT namespace butl // operator will skip symlinks that refer to non-existing or inaccessible // targets. That implies that it will always try to stat() symlinks. // - // Note that we currently do not fully support symlinks on Windows, so the - // ignore_dangling argument affects only directory symlinks (see - // mksymlink() for details). - // explicit dir_iterator (const dir_path&, bool ignore_dangling); diff --git a/libbutl/process.cxx b/libbutl/process.cxx index d4322a9..142d159 100644 --- a/libbutl/process.cxx +++ b/libbutl/process.cxx @@ -1181,49 +1181,6 @@ namespace butl return process_path (); } - class auto_handle - { - public: - explicit - auto_handle (HANDLE h = INVALID_HANDLE_VALUE) noexcept: handle_ (h) {} - - auto_handle (const auto_handle&) = delete; - auto_handle& operator= (const auto_handle&) = delete; - - ~auto_handle () noexcept {reset ();} - - HANDLE - get () const noexcept {return handle_;} - - HANDLE - release () noexcept - { - HANDLE r (handle_); - handle_ = INVALID_HANDLE_VALUE; - return r; - } - - void - reset (HANDLE h = INVALID_HANDLE_VALUE) noexcept - { - if (handle_ != INVALID_HANDLE_VALUE) - { - bool r (CloseHandle (handle_)); - - // The valid process, thread or file handle that has no IO operations - // being performed on it should close successfully, unless something - // is severely damaged. - // - assert (r); - } - - handle_ = h; - } - - private: - HANDLE handle_; - }; - // Make handles inheritable. The process_spawn_mutex must be pre-acquired for // exclusive access. Revert handles inheritability state in destructor. // diff --git a/libbutl/win32-utility.cxx b/libbutl/win32-utility.cxx index b4dda6a..3b44d60 100644 --- a/libbutl/win32-utility.cxx +++ b/libbutl/win32-utility.cxx @@ -9,9 +9,14 @@ #ifdef _WIN32 #ifndef __cpp_lib_modules_ts +#include #include // unique_ptr + +#include // throw_system_error() #else import std.core; + +import butl.utility; #endif using namespace std; @@ -20,6 +25,25 @@ namespace butl { namespace win32 { + const nullhandle_t nullhandle (INVALID_HANDLE_VALUE); + + void auto_handle:: + close () + { + if (handle_ != INVALID_HANDLE_VALUE) + { + bool r (CloseHandle (handle_)); + + // If CloseHandle() failed then no reason to expect it to succeed the + // next time. + // + handle_ = INVALID_HANDLE_VALUE; + + if (!r) + throw_system_error (GetLastError ()); + } + } + struct msg_deleter { void operator() (char* p) const {LocalFree (p);} @@ -40,7 +64,7 @@ namespace butl (char*)&msg, 0, 0)) - return "unknown error code " + to_string (code); + return "unknown error code " + std::to_string (code); unique_ptr m (msg); return msg; diff --git a/libbutl/win32-utility.hxx b/libbutl/win32-utility.hxx index 70aafc4..b71eb1a 100644 --- a/libbutl/win32-utility.hxx +++ b/libbutl/win32-utility.hxx @@ -43,6 +43,86 @@ namespace butl { namespace win32 { + // RAII type for handles. Note that failure to close the handle is + // silently ignored by both the destructor and reset(). + // + // The handle can be INVALID_HANDLE_VALUE. Such a handle is treated as + // unopened and is not closed. + // + struct nullhandle_t + { + constexpr explicit nullhandle_t (HANDLE) {} + operator HANDLE () const {return INVALID_HANDLE_VALUE;} + }; + + LIBBUTL_SYMEXPORT extern const nullhandle_t nullhandle; + + class LIBBUTL_SYMEXPORT auto_handle + { + public: + auto_handle (nullhandle_t = nullhandle) noexcept + : handle_ (INVALID_HANDLE_VALUE) {} + + explicit + auto_handle (HANDLE h) noexcept: handle_ (h) {} + + auto_handle (auto_handle&& h) noexcept: handle_ (h.release ()) {} + auto_handle& operator= (auto_handle&&) noexcept; + + auto_handle (const auto_handle&) = delete; + auto_handle& operator= (const auto_handle&) = delete; + + ~auto_handle () noexcept; + + HANDLE + get () const noexcept {return handle_;} + + void + reset (HANDLE h = INVALID_HANDLE_VALUE) noexcept; + + HANDLE + release () noexcept + { + HANDLE r (handle_); + handle_ = INVALID_HANDLE_VALUE; + return r; + } + + // Close an open handle. Throw std::system_error on the underlying OS + // error. Reset the descriptor to INVALID_HANDLE_VALUE whether the + // exception is thrown or not. + // + void + close (); + + private: + HANDLE handle_; + }; + + inline bool + operator== (const auto_handle& x, const auto_handle& y) + { + return x.get () == y.get (); + } + + inline bool + operator!= (const auto_handle& x, const auto_handle& y) + { + return !(x == y); + } + + inline bool + operator== (const auto_handle& x, nullhandle_t) + { + return x.get () == INVALID_HANDLE_VALUE; + } + + inline bool + operator!= (const auto_handle& x, nullhandle_t y) + { + return !(x == y); + } + LIBBUTL_SYMEXPORT std::string error_msg (DWORD code); @@ -51,4 +131,5 @@ namespace butl } }; +#include #endif // _WIN32 diff --git a/libbutl/win32-utility.ixx b/libbutl/win32-utility.ixx new file mode 100644 index 0000000..403d986 --- /dev/null +++ b/libbutl/win32-utility.ixx @@ -0,0 +1,32 @@ +// file : libbutl/win32-utility.ixx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +namespace butl +{ + namespace win32 + { + inline void auto_handle:: + reset (HANDLE h) noexcept + { + // Don't check for an error as not much we can do here. + // + if (handle_ != INVALID_HANDLE_VALUE) + CloseHandle (handle_); + + handle_ = h; + } + + inline auto_handle& auto_handle:: + operator= (auto_handle&& h) noexcept + { + reset (h.release ()); + return *this; + } + + inline auto_handle:: + ~auto_handle () noexcept + { + reset (); + } + } +} diff --git a/tests/cpfile/driver.cxx b/tests/cpfile/driver.cxx index ae40b5f..c613b49 100644 --- a/tests/cpfile/driver.cxx +++ b/tests/cpfile/driver.cxx @@ -30,10 +30,7 @@ using namespace butl; static const char text1[] = "ABCDEF\nXYZ"; static const char text2[] = "12345\nDEF"; - -#ifndef _WIN32 static const char text3[] = "XAB\r\n9"; -#endif static string from_file (const path& f) @@ -135,72 +132,85 @@ main () assert (from_file (hlink) == text2); -#ifndef _WIN32 - - // Check that 'from' being a symbolic link is properly resolved. + // Note that on Windows regular file symlinks may not be supported (see + // mksymlink() for details), so the following tests are allowed to fail + // with ENOSYS on Windows. // - 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 '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. The + // std::system_error is thrown as cpfile() fails to obtain permissions for + // the 'from' symlink target. + // + try + { + cpfile (tslink, from, cpflags::none); + assert (false); + } + catch (const system_error&) + { + } } - - // Check that copy fail if 'from' symlink points to non-existent file. The - // std::system_error is thrown as cpfile() fails to obtain permissions for - // the 'from' symlink target. - // - try + catch (const system_error& e) { - cpfile (tslink, from, cpflags::none); +#ifndef _WIN32 assert (false); - } - catch (const system_error&) - { - } +#else + assert (e.code ().category () == generic_category () && + e.code ().value () == ENOSYS); #endif + } rmdir_r (td); } diff --git a/tests/dir-iterator/testscript b/tests/dir-iterator/testscript index b1444ad..720622f 100644 --- a/tests/dir-iterator/testscript +++ b/tests/dir-iterator/testscript @@ -14,42 +14,84 @@ $* a >"reg b" mkdir -p a/b; $* a >"dir b" -# Note that on Windows only directory symlinks are currently supported (see -# mksymlink() for details). -# -: dangling-link +: symlink : -if ($cxx.target.class != 'windows') +: If we are not cross-testing let's test dangling and non-dangling symlynks. +: On Windows that involves mklink command usability test. If we fail to create +: a trial link (say because we are not in the Developer Mode and are running +: non-elevated console), then the test group will be silently skipped. +: +if ($test.target == $build.host) { - +mkdir a - +touch --no-cleanup a/b - +ln -s a/b a/l - +rm a/b + +if ($cxx.target.class != 'windows') + lns = ln -s wd/t wd/l &wd/l + else + echo 'yes' >=t + if cmd /C 'mklink l t' >- 2>- &?l && cat l >'yes' + lns = cmd /C 'mklink wd\l t' &wd/l >- + end - +touch a/c + jnc = cmd /C 'mklink /J wd\l wd\t' &wd/l >- + end - $* ../a >! 2>! != 0 : keep - $* -i ../a >'reg c' : skip -} -else -{ - +mkdir a - +mkdir --no-cleanup a/b - +ln -s a/b a/bl - +rmdir a/b + : symlink + : + if! $empty($lns) + { + : file + : + { + +mkdir wd + +touch --no-cleanup wd/t + +touch wd/f + +$lns + +$* wd >>~%EOO% + %(reg f|reg t|sym reg l)%{3} + EOO + +rm wd/t - +touch a/c + $* ../wd >- 2>! != 0 : keep + $* -i ../wd >'reg f': skip + } - +mkdir a/d - +ln -s a/d a/dl + : dir + : + { + +mkdir wd + +mkdir --no-cleanup wd/t + +mkdir wd/d + +$lns - # On Wine dangling symlinks are not visible (see mksymlink() for details). - # - #$* ../a >! 2>! != 0 : keep + # Note that this test may fail on Windows (see symlinks known issues in + # libbutl/filesystem.mxx). + # + +if ($cxx.target.class != 'windows') + $* wd >>~%EOO% + %(dir d|dir t|sym dir l)%{3} + EOO + end - : skip + +rmdir wd/t + + $* ../wd >- 2>! != 0 : keep + $* -i ../wd >'dir d': skip + } + } + + : junction : - $* -i ../a >>~%EOO% - %(reg c|dir d|sym dir dl)%{3} - EOO + if! $empty($jnc) + { + +mkdir wd + +mkdir --no-cleanup wd/t + +mkdir wd/d + +$jnc + +$* wd >>~%EOO% + %(dir d|dir t|sym dir l)%{3} + EOO + +rmdir wd/t + + $* ../wd >- 2>! != 0 : keep + $* -i ../wd >'dir d': skip + } } diff --git a/tests/fdstream/driver.cxx b/tests/fdstream/driver.cxx index 097383c..3215e02 100644 --- a/tests/fdstream/driver.cxx +++ b/tests/fdstream/driver.cxx @@ -419,26 +419,40 @@ main (int argc, const char* argv[]) ofs.clear (ofdstream::failbit); } -#ifndef _WIN32 - - // Fail for an existing symlink to unexistent file. + // Note that on Windows regular file symlinks may not be supported (see + // mksymlink() for details), so the following tests are allowed to fail + // with ENOSYS on Windows. // - path link (td / path ("link")); - mksymlink (td / path ("unexistent"), link); - try { - fdopen (link, (fdopen_mode::out | - fdopen_mode::create | - fdopen_mode::exclusive)); + // Fail for an existing symlink to unexistent file. + // + path link (td / path ("link")); + mksymlink (td / path ("unexistent"), link); - assert (false); + try + { + fdopen (link, (fdopen_mode::out | + fdopen_mode::create | + fdopen_mode::exclusive)); + + assert (false); + } + catch (const ios::failure&) + { + } } - catch (const ios::failure&) + catch (const system_error& e) { +#ifndef _WIN32 + assert (false); +#else + assert (e.code ().category () == generic_category () && + e.code ().value () == ENOSYS); +#endif } -#else +#ifdef _WIN32 // Check translation modes. // diff --git a/tests/link/driver.cxx b/tests/link/driver.cxx index 96ac880..a77df6f 100644 --- a/tests/link/driver.cxx +++ b/tests/link/driver.cxx @@ -35,18 +35,33 @@ using namespace butl; static const char text[] = "ABCDEF"; +enum class mklink +{ + sym, + hard, + any +}; + static bool -link_file (const path& target, const path& link, bool hard, bool check_content) +link_file (const path& target, const path& link, mklink ml, bool check_content) { try { - if (hard) - mkhardlink (target, link); - else - mksymlink (target, link); + switch (ml) + { + case mklink::sym: mksymlink (target, link); break; + case mklink::hard: mkhardlink (target, link); break; + case mklink::any: mkanylink (target, link, true /* copy */); break; + } } catch (const system_error&) { + //cerr << e << endl; + return false; + } + catch (const pair&) + { + //cerr << e.second << endl; return false; } @@ -139,22 +154,26 @@ main () // Create the file hard link. // - assert (link_file (fp, td / path ("hlink"), true, true)); + assert (link_file (fp, td / path ("hlink"), mklink::hard, true)); #ifndef _WIN32 // Create the file symlink using an absolute path. // - assert (link_file (fp, td / path ("slink"), false, true)); + assert (link_file (fp, td / path ("slink"), mklink::sym, true)); // Create the file symlink using a relative path. // - assert (link_file (fn, td / path ("rslink"), false, true)); + assert (link_file (fn, td / path ("rslink"), mklink::sym, true)); // Create the file symlink using an unexistent file path. // - assert (link_file (fp + "-a", td / path ("sa"), false, false)); + assert (link_file (fp + "-a", td / path ("sa"), mklink::sym, false)); #endif + // Create the file any link. + // + assert (link_file (fp, td / path ("alink"), mklink::any, true)); + // Prepare the target directory. // dir_path dn ("dir"); @@ -169,8 +188,8 @@ main () } #ifndef _WIN32 - assert (link_file (fp, dp / path ("hlink"), true, true)); - assert (link_file (fp, dp / path ("slink"), false, true)); + assert (link_file (fp, dp / path ("hlink"), mklink::hard, true)); + assert (link_file (fp, dp / path ("slink"), mklink::sym, true)); #endif // Create the directory symlink using an absolute path. @@ -250,24 +269,64 @@ main () assert (link_dir (dp, ld, false /* hard */, true /* check_content */)); rmdir_r (dp); - // On Wine dangling junctions are not visible. That's why we also re-create - // the target before the junction removal. - // -#if 0 { pair pe (path_entry (ld)); assert (pe.first && pe.second.type == entry_type::symlink); } -#endif { pair pe (path_entry (ld, true /* follow_symlinks */)); assert (!pe.first); } - assert (try_mkdir (dp) == mkdir_status::success); assert (try_rmsymlink (ld) == rmfile_status::success); + // Try to create a dangling regular file symlink and make sure it is + // properly removed via its parent recursive removal. + // + assert (try_mkdir (dp) == mkdir_status::success); + + // Note that on Windows regular file symlinks may not be supported (see + // mksymlink() for details), so the following tests are allowed to fail + // with ENOSYS on Windows. + // + try + { + mksymlink (dp / "non-existing", dp / "lnk"); + assert (!dir_empty (dp)); + assert (dir_iterator (dp, true /* ignore_dangling */) == dir_iterator ()); + } + catch (const system_error& e) + { +#ifndef _WIN32 + assert (false); +#else + assert (e.code ().category () == generic_category () && + e.code ().value () == ENOSYS); +#endif + } + + rmdir_r (dp); + + // Create a dangling directory symlink and make sure it is properly removed + // via its parent recursive removal. Also make sure that removing directory + // symlink keeps its target intact. + // + assert (try_mkdir (dp) == mkdir_status::success); + + dir_path tgd (td / dir_path ("tdir")); + assert (try_mkdir (tgd) == mkdir_status::success); + + mksymlink (dp / "non-existing", dp / "lnk1", true /* dir */); + assert (!dir_empty (dp)); + assert (dir_iterator (dp, true /* ignore_dangling */) == dir_iterator ()); + + mksymlink (tgd, dp / "lnk2", true /* dir */); + assert (dir_iterator (dp, true /* ignore_dangling */) != dir_iterator ()); + + rmdir_r (dp); + assert (dir_exists (tgd)); + try { rmdir_r (td); diff --git a/tests/mventry/testscript b/tests/mventry/testscript index ecd617a..c6cbf45 100644 --- a/tests/mventry/testscript +++ b/tests/mventry/testscript @@ -92,8 +92,8 @@ : : 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. +: trial link (say because we are not in the Developer Mode and are running +: non-elevated console), then the test group will be silently skipped. : if ($test.target == $build.host) { @@ -104,8 +104,12 @@ if ($test.target == $build.host) if cmd /C 'mklink b a' >- 2>- &?b && cat b >'yes' lns = cmd /C 'mklink b a' >- end + + jnc = cmd /C 'mklink /J b a' >- end + : symlink + : if! $empty($lns) { : file @@ -126,11 +130,11 @@ if ($test.target == $build.host) : to : - : Make sure that if destination is a symlink it is get overwritten and it's - : target stays intact. + : Make sure that if destination is a symlink it is get overwritten and + : it's target stays intact. : echo 'foo' >=a; - $lns; + $lns &b; echo 'bar' >=c &!c; $* c b; cat a >'foo'; @@ -182,6 +186,43 @@ if ($test.target == $build.host) $* b c 2>- == 1 } } + + : junction + : + if! $empty($jnc) + { + : from + : + : Make sure that if source is a junction it refers the same target after + : rename. + : + mkdir -p a; + $jnc &!b; + $* b c &c; + touch a/b; + test -f c/b; + test -d b == 1 + + : to + : + : Make sure that if destination is a junction it is get overwritten and + : it's target stays intact. + : + mkdir -p a; + $jnc; + echo 'foo' >=c &!c; + $* c b &b; + cat b >'foo'; + test -d a; + test -f c == 1 + + : over-existing-dir + : + mkdir a; + $jnc &b; + mkdir c; + $* b c 2>- == 1 + } } : different-fs diff --git a/tests/path-entry/driver.cxx b/tests/path-entry/driver.cxx index d48bf49..51ac04d 100644 --- a/tests/path-entry/driver.cxx +++ b/tests/path-entry/driver.cxx @@ -4,7 +4,9 @@ #include #ifndef __cpp_lib_modules_ts +#include #include +#include // invalid_argument #include #endif @@ -15,48 +17,210 @@ import std.core; import std.io; #endif +import butl.path; import butl.utility; // operator<<(ostream, exception) +import butl.optional; +import butl.timestamp; import butl.filesystem; #else +#include #include +#include +#include #include #endif using namespace std; using namespace butl; -// Usage: argv[0] +// Usage: argv[0] [-l] [-t] [-p ] [-m