aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-03-07 14:07:28 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-03-09 14:18:20 +0300
commitdcccba655fe848564e961b3f285ce3a82d3ac73a (patch)
tree598ced3b406d80c23798672930e1a17cfe112b75
parent63b2988e4f2630cc688ff43b7e5f0d4f977896cd (diff)
Add more support for symlinks on Windows
See mksymlink() for details of the symlinks support on Windows.
-rw-r--r--libbutl/buildfile2
-rw-r--r--libbutl/fdstream.cxx33
-rw-r--r--libbutl/filesystem.cxx1089
-rw-r--r--libbutl/filesystem.mxx72
-rw-r--r--libbutl/process.cxx43
-rw-r--r--libbutl/win32-utility.cxx26
-rw-r--r--libbutl/win32-utility.hxx81
-rw-r--r--libbutl/win32-utility.ixx32
-rw-r--r--tests/cpfile/driver.cxx134
-rw-r--r--tests/dir-iterator/testscript100
-rw-r--r--tests/fdstream/driver.cxx38
-rw-r--r--tests/link/driver.cxx93
-rw-r--r--tests/mventry/testscript51
-rw-r--r--tests/path-entry/driver.cxx212
-rw-r--r--tests/path-entry/testscript175
-rw-r--r--tests/wildcard/testscript52
16 files changed, 1532 insertions, 701 deletions
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<bool, entry_stat> 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<uint64_t> (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<permissions> (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<mode_t> (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 <typename S>
+ 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 <typename S>
+ static inline constexpr auto
+ mnsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec)
+ {
+ return s->st_mtimespec.tv_nsec; // *BSD, MacOS.
+ }
+
+ template <typename S>
+ 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 <typename S>
+ // static inline constexpr int
+ // mnsec (...) {return 0;}
+
+ template <typename S>
+ 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 <typename S>
+ static inline constexpr auto
+ ansec (const S* s, int) -> decltype(s->st_atimespec.tv_nsec)
+ {
+ return s->st_atimespec.tv_nsec; // *BSD, MacOS.
+ }
+
+ template <typename S>
+ 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 <typename S>
+ // 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<duration> (chrono::nanoseconds (nsec));
+ };
+
+ return {tm (s.st_mtime, mnsec<struct stat> (&s, true)),
+ tm (s.st_atime, ansec<struct stat> (&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<usec_type> (nsec / 1000)};
+
+ uint64_t usec (chrono::duration_cast<chrono::microseconds> (
+ t.time_since_epoch ()).count ());
+
+ return {static_cast<time_t> (usec / 1000000), // Seconds.
+ static_cast<usec_type> (usec % 1000000)}; // Microseconds.
+ };
+
+ timeval times[2];
+ times[0] = tm (t.access, s.st_atime, ansec<struct stat> (&s, true));
+ times[1] = tm (t.modification, s.st_mtime, mnsec<struct stat> (&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<win32::auto_handle, BY_HANDLE_FILE_INFORMATION>
+ 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<win32::auto_handle, BY_HANDLE_FILE_INFORMATION>
+ 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<bool, BY_HANDLE_FILE_INFORMATION>
+ path_entry_info (const char* p, bool ie = false)
+ {
+ using namespace win32;
+
+ pair<auto_handle, BY_HANDLE_FILE_INFORMATION> 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<bool, BY_HANDLE_FILE_INFORMATION>
+ path_entry_info (const path& p, bool ie = false)
{
- return junction_target_exists (p.string ().c_str (), ignore_error);
+ return path_entry_info (p.string ().c_str (), ie);
+ }
+
+ // As above but return entry_stat.
+ //
+ static inline pair<bool, entry_stat>
+ path_entry_stat (const char* p, bool ie)
+ {
+ pair<bool, BY_HANDLE_FILE_INFORMATION> 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<bool, entry_stat>
+ path_entry_stat (const path& p, bool ie)
+ {
+ return path_entry_stat (p.string ().c_str (), ie);
}
pair<bool, entry_stat>
@@ -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.
+ //
+ }
+
+ // 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<bool, BY_HANDLE_FILE_INFORMATION> 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<chrono::nanoseconds> (
+ t.time_since_epoch ()).count ());
- entry_type et (entry_type::unknown);
- struct __stat64 s; // For 64-bit size.
+ ticks /= 100; // Now in 100 nanosecond "ticks".
+ ticks += 11644473600ULL * 10000000; // Now in "Windows epoch".
- if (_stat64 (p, &s) != 0)
+ 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<auto_handle, BY_HANDLE_FILE_INFORMATION> hi (
+ entry_info_handle (p, true /* write */));
+
+ 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<bool, BY_HANDLE_FILE_INFORMATION> 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<uint64_t> (t.dwHighDateTime) << 32) |
+ t.dwLowDateTime);
+
+ nsec -= 11644473600ULL * 10000000; // Now in UNIX epoch.
+ nsec *= 100; // Now in nanoseconds.
+
+ return timestamp (
+ chrono::duration_cast<duration> (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<auto_handle, BY_HANDLE_FILE_INFORMATION> 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;
- return make_pair (true,
- entry_stat {et, static_cast<uint64_t> (s.st_size)});
+ ft = to_filetime (t);
+ return &ft;
+ };
+
+ 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<dir_path> (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<HANDLE, void (*)(HANDLE*)> 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<size_t> (-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<size_t> (-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<WORD> (n * sizeof (wchar_t)));
+ };
- if (s != NULL) // Not enough space in the destination buffer.
- throw_generic_error (ENAMETOOLONG);
+ // Convert the junction target path (\??\C:\...\).
+ //
+ pair<size_t, WORD> np (conv ("\\??\\" + atd.string () + "\\",
+ rb.path_buffer));
+
+ // Convert the junction target print name (C:\...).
+ //
+ pair<size_t, WORD> 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<WORD> (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.
@@ -621,16 +1089,9 @@ namespace butl
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");
+ h.close (); // Checks for error.
- return try_rmfile (link, ie);
+ 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 <typename S>
- 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 <typename S>
- static inline constexpr auto
- mnsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec)
- {
- return s->st_mtimespec.tv_nsec; // *BSD, MacOS.
- }
-
- template <typename S>
- 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 <typename S>
- // static inline constexpr int
- // mnsec (...) {return 0;}
-
- template <typename S>
- 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 <typename S>
- static inline constexpr auto
- ansec (const S* s, int) -> decltype(s->st_atimespec.tv_nsec)
- {
- return s->st_atimespec.tv_nsec; // *BSD, MacOS.
- }
-
- template <typename S>
- 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 <typename S>
- // 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<dir_path> (to)) != rmdir_status::not_empty)
+ if (ec == ERROR_ALREADY_EXISTS || ec == ERROR_ACCESS_DENIED)
{
- if (MoveFileExA (f, t, mfl))
- return;
+ bool retry (false);
- ec = GetLastError ();
+ if (te.first && te.second.type == entry_type::symlink && junction (t))
+ {
+ try_rmsymlink (to);
+ retry = true;
+ }
+ else if (td)
+ retry = try_rmdir (path_cast<dir_path> (to)) !=
+ rmdir_status::not_empty;
+
+ if (retry)
+ {
+ if (MoveFileExA (f, t, mfl))
+ return;
+
+ 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<duration> (chrono::nanoseconds (nsec));
- };
-
- return {tm (s.st_mtime, mnsec<struct stat> (&s, true)),
- tm (s.st_atime, ansec<struct stat> (&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<uint64_t> (t.dwHighDateTime) << 32) |
- t.dwLowDateTime);
-
- nsec -= 11644473600ULL * 10000000; // Now in UNIX epoch.
- nsec *= 100; // Now in nanoseconds.
-
- return timestamp (
- chrono::duration_cast<duration> (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<usec_type> (nsec / 1000)};
-
- uint64_t usec (chrono::duration_cast<chrono::microseconds> (
- t.time_since_epoch ()).count ());
-
- return {static_cast<time_t> (usec / 1000000), // Seconds.
- static_cast<usec_type> (usec % 1000000)}; // Microseconds.
- };
-
- timeval times[2];
- times[0] = tm (t.access, s.st_atime, ansec<struct stat> (&s, true));
- times[1] = tm (t.modification, s.st_mtime, mnsec<struct stat> (&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<HANDLE, void (*)(HANDLE*)> 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<chrono::nanoseconds> (
- 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<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_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<bool, entry_stat> e (path_entry (p, link));
+ pair<bool, entry_stat> 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<bool, entry_stat> 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<bool, entry_stat>
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 <string>
#include <memory> // unique_ptr
+
+#include <libbutl/utility.mxx> // 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<char, msg_deleter> 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 <libbutl/win32-utility.ixx>
#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<entry_type, system_error>&)
+ {
+ //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<bool, entry_stat> pe (path_entry (ld));
assert (pe.first && pe.second.type == entry_type::symlink);
}
-#endif
{
pair<bool, entry_stat> 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 <cassert>
#ifndef __cpp_lib_modules_ts
+#include <string>
#include <iostream>
+#include <stdexcept> // invalid_argument
#include <system_error>
#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 <libbutl/path.mxx>
#include <libbutl/utility.mxx>
+#include <libbutl/optional.mxx>
+#include <libbutl/timestamp.mxx>
#include <libbutl/filesystem.mxx>
#endif
using namespace std;
using namespace butl;
-// Usage: argv[0] <path>
+// Usage: argv[0] [-l] [-t] [-p <permissions>] [-m <time>] [-a <time>] <path>
//
-// If path entry exists then print it's type and size (meaningful for the
-// regular file only) to STDOUT, and exit with the zero code. Otherwise exit
-// with the one code. Don't follow symlink. On failure print the error
-// description to STDERR and exit with the two code.
+// If path entry exists then optionally modify its meta-information and print
+// its type, size (meaningful for the regular file only), permissions,
+// modification and access times to STDOUT, one value per line, and exit with
+// the zero code. Otherwise exit with the one code. Don't follow symlink by
+// default. On failure print the error description to STDERR and exit with
+// the two code.
+//
+// -l
+// Follow symlinks.
+//
+// -t
+// Assume the path is a file and touch it. Implies -l.
+//
+// -p <permissions>
+// Set path permissions specified in the chmod utility octal form. Implies
+// -l.
+//
+// -m <time>
+// Set path modification time specified in the "%Y-%m-%d %H:%M:%S%[.N]"
+// format. Implies -l.
+//
+// -a <time>
+// As -m but set the access time.
//
int
main (int argc, const char* argv[])
-try
{
- assert (argc == 2);
+ string stage;
+
+ try
+ {
+ using butl::optional;
+
+ bool follow_symlinks (false);
+ optional<permissions> perms;
+ optional<timestamp> mtime;
+ optional<timestamp> atime;
+ bool touch (false);
+
+ auto time = [] (const char* v)
+ {
+ return from_string (v, "%Y-%m-%d %H:%M:%S%[.N]", true /* local */);
+ };
+
+ int i (1);
+ for (; i != argc; ++i)
+ {
+ string v (argv[i]);
+
+ if (v == "-l")
+ follow_symlinks = true;
+ else if (v == "-t")
+ {
+ touch = true;
+ follow_symlinks = true;
+ }
+ else if (v == "-p")
+ {
+ assert (++i != argc);
+ v = argv[i];
+
+ size_t n;
+ perms = static_cast<permissions> (stoull (v, &n, 8));
+ assert (n == v.size ());
+
+ follow_symlinks = true;
+ }
+ else if (v == "-m")
+ {
+ assert (++i != argc);
+ mtime = time (argv[i]);
+
+ follow_symlinks = true;
+ }
+ else if (v == "-a")
+ {
+ assert (++i != argc);
+ atime = time (argv[i]);
+
+ follow_symlinks = true;
+ }
+ else
+ break;
+ }
+
+ assert (i == argc - 1);
+
+ path p (argv[i]);
+
+ if (touch)
+ {
+ stage = "touch";
+ touch_file (p);
+ }
+
+ stage = "stat entry";
+ pair<bool, entry_stat> es (path_entry (p, follow_symlinks));
- auto es (path_entry (argv[1]));
+ if (!es.first)
+ return 1;
- if (!es.first)
- return 1;
+ // The entry is a directory with a symlink followed.
+ //
+ bool tdir;
- switch (es.second.type)
+ if (follow_symlinks || es.second.type != entry_type::symlink)
+ tdir = (es.second.type == entry_type::directory);
+ else
+ {
+ stage = "stat target";
+ pair<bool, entry_stat> ts (path_entry (p, true /* follow_symlinks */));
+
+ if (!ts.first)
+ return 1;
+
+ tdir = (ts.second.type == entry_type::directory);
+ }
+
+ if (perms)
+ {
+ stage = "set permissions";
+ path_permissions (p, *perms);
+ }
+
+ if (mtime)
+ {
+ if (tdir)
+ {
+ stage = "set directory mtime";
+ dir_mtime (path_cast<dir_path> (p), *mtime);
+ }
+ else
+ {
+ stage = "set file mtime";
+ file_mtime (p, *mtime);
+ }
+ }
+
+ if (atime)
+ {
+ if (tdir)
+ {
+ stage = "set directory atime";
+ dir_atime (path_cast<dir_path> (p), *atime);
+ }
+ else
+ {
+ stage = "set file atime";
+ file_atime (p, *atime);
+ }
+ }
+
+ cout << "type: ";
+
+ switch (es.second.type)
+ {
+ case entry_type::unknown: cout << "unknown"; break;
+ case entry_type::regular: cout << "regular"; break;
+ case entry_type::directory: cout << "directory"; break;
+ case entry_type::symlink: cout << "symlink"; break;
+ case entry_type::other: cout << "other"; break;
+ }
+
+ stage = "get permissions";
+
+ cout << endl
+ << "size: " << es.second.size << endl
+ << "permissions: " << oct
+ << static_cast<size_t> (path_permissions (p)) << endl;
+
+ stage = tdir ? "get directory times" : "get file times";
+
+ entry_time et (tdir ? dir_time (path_cast<dir_path> (p)) : file_time (p));
+ cout << "mtime: " << et.modification << endl
+ << "atime: " << et.access << endl;
+
+ return 0;
+ }
+ catch (const invalid_argument& e)
{
- case entry_type::unknown: cout << "unknown"; break;
- case entry_type::regular: cout << "regular"; break;
- case entry_type::directory: cout << "directory"; break;
- case entry_type::symlink: cout << "symlink"; break;
- case entry_type::other: cout << "other"; break;
+ cerr << e << endl;
+ return 2;
+ }
+ catch (const system_error& e)
+ {
+ cerr << stage << " failed: " << e << endl;
+ return 2;
}
-
- cout << endl << es.second.size << endl;
- return 0;
-}
-catch (const system_error& e)
-{
- cerr << e << endl;
- return 2;
}
diff --git a/tests/path-entry/testscript b/tests/path-entry/testscript
index 456f96f..76316bf 100644
--- a/tests/path-entry/testscript
+++ b/tests/path-entry/testscript
@@ -10,21 +10,176 @@
: printed on Windows. This why we exclude it, to get consistent behavior on
: both POSIX and Windows.
:
- cat <:'abc' >=f;
- $* f >>EOO
- regular
- 3
- EOO
+ {
+ cat <:'abc' >=f;
+ $* f >>~/EOO/
+ type: regular
+ size: 3
+ /.+
+ EOO
+ }
: dir
:
: Note that the size value is meaningless for directory entries.
:
- mkdir -p d;
- $* d >>~/EOO/
- directory
- /.
- EOO
+ {
+ mkdir -p d;
+ $* d >>~/EOO/
+ type: directory
+ /.+
+ EOO
+ }
+
+ : followed-symlink
+ :
+ {
+ cat <:'abc' >=f;
+ ln -s f l;
+ $* -l l >>~/EOO/
+ type: regular
+ size: 3
+ /.+
+ EOO
+ }
+
+ : symlink
+ :
+ : If we are not cross-testing let's test if symlinks are properly followed.
+ : 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)
+ {
+ +if ($cxx.target.class != 'windows')
+ lns = ln -s t l &l
+ else
+ echo 'yes' >=t
+ if cmd /C 'mklink l t' >- 2>- &?l && cat l >'yes'
+ lns = cmd /C 'mklink l t' &l >-
+ end
+
+ jnc = cmd /C 'mklink /J l t' &l >-
+ end
+
+ : symlink
+ :
+ if! $empty($lns)
+ {
+ : file
+ :
+ {
+ : get-info
+ :
+ : Test that the target type, size, permissions and file times are
+ : obtained via a symlink.
+ :
+ {
+ cat <:'abc' >=t;
+ $lns;
+ $* -p 400 -m '2020-03-05 00:00:00' -a '2020-03-05 00:00:01' t | set ti;
+ $* -l l >"$ti"
+ }
+
+ : set-info
+ :
+ : Test that permissions and file times are set via a symlink.
+ :
+ {
+ cat <:'abc' >=t;
+ $lns;
+ $* -p 400 -m '2020-03-05 00:00:00' -a '2020-03-05 00:00:01' l | set ti;
+ sed -n -e 's/permissions: (.+)/\1/p' <"$ti" >~/'4.{2}'/;
+ sed -n -e 's/mtime: (.+)/\1/p' <"$ti" >'2020-03-05 00:00:00';
+ sed -n -e 's/atime: (.+)/\1/p' <"$ti" >'2020-03-05 00:00:01'
+ }
+
+ : touch
+ :
+ : Test that a symlink touch touches the target.
+ :
+ {
+ cat <:'abc' >=t;
+ $lns;
+ $* t | set ti;
+ sleep 2;
+ $* -t l | set li;
+ if ("$ti" == "$li")
+ exit "link touch doesn't change target"
+ end
+ }
+ }
+
+ : dir
+ :
+ : Note that the following tests may fail on Windows (see symlinks known
+ : issues in libbutl/filesystem.mxx).
+ :
+ if ($cxx.target.class != 'windows')
+ {
+ : get-info
+ :
+ : Test that the target type, size, permissions and file times are
+ : obtained via a symlink.
+ :
+ {
+ mkdir t;
+ $lns;
+ $* -p 400 -m '2020-03-05 00:00:00' -a '2020-03-05 00:00:01' t | set ti;
+ $* -l l >"$ti";
+ $* -p 600 t >- # @@ TMP; until build2 is staged.
+ }
+
+ : set-info
+ :
+ : Test that permissions and file times are set via a symlink.
+ :
+ {
+ mkdir t;
+ $lns;
+ $* -p 400 -m '2020-03-05 00:00:00' -a '2020-03-05 00:00:01' l | set ti;
+ sed -n -e 's/permissions: (.+)/\1/p' <"$ti" >~/'4.{2}'/;
+ sed -n -e 's/mtime: (.+)/\1/p' <"$ti" >'2020-03-05 00:00:00';
+ sed -n -e 's/atime: (.+)/\1/p' <"$ti" >'2020-03-05 00:00:01';
+ $* -p 600 t >- # @@ TMP; until build2 is staged.
+ }
+ }
+ }
+
+ : junction
+ :
+ if! $empty($jnc)
+ {
+ : get-info
+ :
+ : Test that the target type, size, permissions and file times are
+ : obtained via a junction.
+ :
+ {
+ mkdir t;
+ $jnc;
+ $* -p 400 -m '2020-03-05 00:00:00' -a '2020-03-05 00:00:01' t | set ti;
+ $* -l l >"$ti";
+ $* -p 600 t >- # @@ TMP; until build2 is staged.
+ }
+
+ : set-info
+ :
+ : Test that permissions and file times are set via a junction.
+ :
+ {
+ mkdir t;
+ $jnc;
+ $* -p 400 -m '2020-03-05 00:00:00' -a '2020-03-05 00:00:01' l | set ti;
+ sed -n -e 's/permissions: (.+)/\1/p' <"$ti" >~/'4.{2}'/;
+ sed -n -e 's/mtime: (.+)/\1/p' <"$ti" >'2020-03-05 00:00:00';
+ sed -n -e 's/atime: (.+)/\1/p' <"$ti" >'2020-03-05 00:00:01';
+ $* -p 600 t >- # @@ TMP; until build2 is staged.
+ }
+ }
+ }
}
: non-existent
diff --git a/tests/wildcard/testscript b/tests/wildcard/testscript
index e1ddd5a..3590aa3 100644
--- a/tests/wildcard/testscript
+++ b/tests/wildcard/testscript
@@ -149,6 +149,10 @@
: path-directory-search
:
+: Note that we always need to make sure that no auxiliary files (stdout, etc)
+: are created in the directories we search through not to end up with a race
+: condition (trying to query type of a removed filesystem entry, etc).
+:
{
test.options = -sd
@@ -294,19 +298,21 @@
: file
:
{
- +mkdir -p foo fox fix/bar baz/foo/zab baz/foo/zab/baz
- +touch foo/bar foo/fox fox/baz baz/foo/zab/bar
+ +mkdir -p wd/foo wd/fox wd/fix/bar wd/baz/foo/zab wd/baz/foo/zab/baz
+ +touch wd/foo/bar wd/foo/fox wd/fox/baz wd/baz/foo/zab/bar
+
+ wd = ../wd
: immediate
:
- $* f*/b* .. >>/EOO
+ $* f*/b* $wd >>/EOO
foo/bar
fox/baz
EOO
: recursive
:
- $* f**/b** .. >>/EOO
+ $* f**/b** $wd >>/EOO
baz/foo/zab/bar
foo/bar
fox/baz
@@ -315,15 +321,17 @@
: self-recursive
:
{
+ wd = ../../wd
+
: pattern
:
- $* foo/f*** ../.. >>/EOO
+ $* foo/f*** $wd >>/EOO
foo/fox
EOO
: start
:
- $* f*** ../../foo >>/EOO
+ $* f*** $wd/foo >>/EOO
fox
EOO
@@ -340,19 +348,21 @@
: dir
:
{
- +mkdir -p foo/bar foo/fox/box fox/baz fix baz/foo/zab/baz
- +touch fix/bar baz/foo/zab/bar
+ +mkdir -p wd/foo/bar wd/foo/fox/box wd/fox/baz wd/fix wd/baz/foo/zab/baz
+ +touch wd/fix/bar wd/baz/foo/zab/bar
+
+ wd = ../wd
: immediate
:
- $* f*/b*/ .. >>/EOO
+ $* f*/b*/ $wd >>/EOO
foo/bar/
fox/baz/
EOO
: recursive
:
- $* f**/b**/ .. >>/EOO
+ $* f**/b**/ $wd >>/EOO
baz/foo/zab/baz/
foo/bar/
foo/fox/box/
@@ -363,9 +373,11 @@
: self-recursive
:
{
+ wd = ../../wd
+
: pattern
:
- $* foo/f***/b**/ ../.. >>/EOO
+ $* foo/f***/b**/ $wd >>/EOO
foo/bar/
foo/fox/box/
foo/fox/box/
@@ -373,7 +385,7 @@
: start
:
- $* f***/b**/ ../../foo >>/EOO
+ $* f***/b**/ $wd/foo >>/EOO
bar/
fox/box/
fox/box/
@@ -394,13 +406,13 @@
: fast-forward
:
{
- +mkdir -p foo/bar/baz foo/box
- +touch foo/bar/baz/fox
+ +mkdir -p wd/foo/bar/baz wd/foo/box
+ +touch wd/foo/bar/baz/fox
: partial
:
{
- wd = ../..
+ wd = ../../wd
: file
:
@@ -418,7 +430,7 @@
: reduce
:
{
- wd = ../../..
+ wd = ../../../wd
: exists
:
@@ -457,11 +469,11 @@
: dot-started
:
{
- +mkdir -p z/.z/.z z/z a/.z .a/.z
- +touch z/.z.cxx z/z.cxx z/.z/.z.cxx z/.z/z.cxx z/z/.z.cxx z/z/z.cxx \
- a/z.cxx a/.z.cxx .a/z.cxx .a/.z.cxx
+ +mkdir -p wd/z/.z/.z wd/z/z wd/a/.z wd/.a/.z
+ +touch wd/z/.z.cxx wd/z/z.cxx wd/z/.z/.z.cxx wd/z/.z/z.cxx wd/z/z/.z.cxx \
+ wd/z/z/z.cxx wd/a/z.cxx wd/a/.z.cxx wd/.a/z.cxx wd/.a/.z.cxx
- wd=../../..
+ wd=../../../wd
: recursive
: