aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2018-04-28 17:58:36 +0300
committerBoris Kolpackov <boris@codesynthesis.com>2018-04-28 17:35:54 +0200
commit085493111005770ed33beeba07d317b6eba0c851 (patch)
tree669bfadb85390728e93338a8a352e2f1dedae11e
parenteba3042910f063ae638a7e0134b79175978e2fca (diff)
Add support for directory symlinks on Windows
-rw-r--r--libbutl/filesystem.cxx193
-rw-r--r--libbutl/filesystem.mxx36
-rw-r--r--tests/link/driver.cxx64
3 files changed, 261 insertions, 32 deletions
diff --git a/libbutl/filesystem.cxx b/libbutl/filesystem.cxx
index 6557e7a..7ee4c2c 100644
--- a/libbutl/filesystem.cxx
+++ b/libbutl/filesystem.cxx
@@ -21,10 +21,13 @@
# include <io.h> // _find*(), _unlink(), _chmod()
# include <direct.h> // _mkdir(), _rmdir()
+# include <winioctl.h> // FSCTL_SET_REPARSE_POINT
# include <sys/types.h> // _stat
# include <sys/stat.h> // _stat(), S_I*
# include <sys/utime.h> // _utime()
+# include <cwchar> // mbsrtowcs(), mbstate_t
+
# ifdef _MSC_VER // Unlikely to be fixed in newer versions.
# define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
@@ -124,7 +127,7 @@ namespace butl
}
#else
pair<bool, entry_stat>
- path_entry (const char* p, bool, bool ie)
+ path_entry (const char* p, bool fl, bool ie)
{
// A path like 'C:', while being a root path in our terminology, is not as
// such for Windows, that maintains current directory for each drive, and
@@ -140,6 +143,18 @@ namespace butl
p = d.c_str ();
}
+ struct __stat64 s; // For 64-bit size.
+
+ if (_stat64 (p, &s) != 0)
+ {
+ if (errno == ENOENT || errno == ENOTDIR || ie)
+ return make_pair (false, entry_stat {entry_type::unknown, 0});
+ else
+ throw_generic_error (errno);
+ }
+
+ auto m (s.st_mode);
+
DWORD attr (GetFileAttributesA (p));
if (attr == INVALID_FILE_ATTRIBUTES) // Presumably not exists.
return make_pair (false, entry_stat {entry_type::unknown, 0});
@@ -147,24 +162,11 @@ namespace butl
entry_type et (entry_type::unknown);
uint64_t es (0);
- // S_ISLNK/S_IFDIR are not defined for Win32 but it does have symlinks.
- // We will consider symlink entry to be of the unknown type. Note that
- // S_ISREG() and S_ISDIR() return as they would do for a symlink target.
+ // Note that we currently support only directory symlinks (see mksymlink()
+ // function description for more details).
//
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
{
- struct __stat64 s; // For 64-bit size.
-
- if (_stat64 (p, &s) != 0)
- {
- if (errno == ENOENT || errno == ENOTDIR || ie)
- return make_pair (false, entry_stat {entry_type::unknown, 0});
- else
- throw_generic_error (errno);
- }
-
- auto m (s.st_mode);
-
if (S_ISREG (m))
et = entry_type::regular;
else if (S_ISDIR (m))
@@ -175,6 +177,15 @@ namespace butl
es = static_cast<uint64_t> (s.st_size);
}
+ else
+ {
+ // @@ If we follow symlinks, then we also need to check if the target
+ // exists. The implementation is a bit hairy, so let's do when
+ // required.
+ //
+ if (S_ISDIR (m))
+ et = fl ? entry_type::directory : entry_type::symlink;
+ }
return make_pair (true, entry_stat {et, es});
}
@@ -343,6 +354,7 @@ namespace butl
}
#ifndef _WIN32
+
void
mksymlink (const path& target, const path& link, bool)
{
@@ -350,6 +362,12 @@ namespace butl
throw_generic_error (errno);
}
+ rmfile_status
+ rmsymlink (const path& link, bool)
+ {
+ return try_rmfile (link);
+ }
+
void
mkhardlink (const path& target, const path& link, bool)
{
@@ -360,9 +378,145 @@ namespace butl
#else
void
- mksymlink (const path&, const path&, bool)
+ mksymlink (const path& target, const path& link, bool dir)
{
- throw_generic_error (ENOSYS, "symlinks not supported");
+ if (!dir)
+ throw_generic_error (ENOSYS, "file symlinks not supported");
+
+ dir_path ld (path_cast<dir_path> (link));
+
+ mkdir_status rs (try_mkdir (ld));
+
+ if (rs == mkdir_status::already_exists)
+ throw_generic_error (EEXIST);
+
+ assert (rs == mkdir_status::success);
+
+ // We need to be careful with the directory auto-removal since it is
+ // recursive. So we must cancel it right after the reparse point target
+ // is setup for the directory path.
+ //
+ 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));
+
+ 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);
+ });
+
+ // We will fill the uninitialized members later.
+ //
+ struct
+ {
+ // Header.
+ //
+ DWORD reparse_tag = IO_REPARSE_TAG_MOUNT_POINT;
+ WORD reparse_data_length;
+ WORD reserved = 0;
+
+ // Mount point reparse buffer.
+ //
+ WORD substitute_name_offset = 0;
+ WORD substitute_name_length;
+ WORD print_name_offset;
+ WORD print_name_length = 0;
+
+ // Reserve space for two NULL characters (for the names above).
+ //
+ wchar_t path_buffer[MAX_PATH + 2];
+ } rb;
+
+ // Make the target path absolute, decorate it and convert to a
+ // wide-character string.
+ //
+ path atd (target);
+
+ if (atd.relative ())
+ atd.complete ();
+
+ string td ("\\??\\" + atd.string () + "\\");
+ const char* s (td.c_str ());
+
+ // Zero-initialize the conversion state (and disambiguate with the
+ // function declaration).
+ //
+ mbstate_t state ((mbstate_t ()));
+ size_t n (mbsrtowcs (rb.path_buffer, &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);
+
+ // Fill the rest of the structure and setup the reparse point.
+ //
+ // The path length not including NULL character, in bytes.
+ //
+ WORD nb (static_cast<WORD> (n) * sizeof (wchar_t));
+ rb.substitute_name_length = nb;
+
+ // The print name offset, in bytes.
+ //
+ rb.print_name_offset = nb + sizeof (wchar_t);
+ rb.path_buffer[n + 1] = L'\0'; // Terminate the (empty) print name.
+
+ // 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.
+
+ DWORD r;
+ if (!DeviceIoControl (
+ h,
+ FSCTL_SET_REPARSE_POINT,
+ &rb,
+ sizeof (DWORD) + 2 * sizeof (WORD) + // Size of the header.
+ rb.reparse_data_length, // Size of mount point reparse.
+ NULL, // buffer.
+ 0,
+ &r,
+ NULL))
+ throw_system_error (GetLastError ());
+
+ rm.cancel ();
+ }
+
+ rmfile_status
+ rmsymlink (const path& link, bool dir)
+ {
+ if (!dir)
+ throw_generic_error (ENOSYS, "file symlinks not supported");
+
+ switch (try_rmdir (path_cast<dir_path> (link)))
+ {
+ case rmdir_status::success: return rmfile_status::success;
+ case rmdir_status::not_exist: return rmfile_status::not_exist;
+ case rmdir_status::not_empty: throw_generic_error (ENOTEMPTY);
+ }
+
+ assert (false); // Can't be here.
+ return rmfile_status::success;
}
void
@@ -1163,7 +1317,8 @@ namespace butl
e_.p_ = move (p);
- // We do not support symlinks at the moment.
+ // Note that while we support directory symlinks, they are not seen
+ // here (see mksymlink() function description for details).
//
e_.t_ = fi.attrib & _A_SUBDIR
? entry_type::directory
diff --git a/libbutl/filesystem.mxx b/libbutl/filesystem.mxx
index 7f93739..8d99fdc 100644
--- a/libbutl/filesystem.mxx
+++ b/libbutl/filesystem.mxx
@@ -236,7 +236,21 @@ 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 Windows symlinks are currently not supported.
+ // 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.
+ //
+ // - 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 rmsymlink().
+ //
+ // - Symlinks are not visible when iterating over a directory with
+ // dir_iterator. As a result, a directory that contains symlinks can not
+ // be recursively deleted.
//
LIBBUTL_SYMEXPORT void
mksymlink (const path& target, const path& link, bool dir = false);
@@ -250,12 +264,26 @@ LIBBUTL_MODEXPORT namespace butl
mksymlink (target, link, true);
}
+ // Remove a symbolic link to a file (default) or directory (third argument
+ // is true). Throw std::system_error on failures.
+ //
+ LIBBUTL_SYMEXPORT rmfile_status
+ rmsymlink (const path&, bool dir = false);
+
+ // Remove a symbolic link to a directory. Throw std::system_error on
+ // failures.
+ //
+ inline rmfile_status
+ rmsymlink (const dir_path& link)
+ {
+ return rmsymlink (link, true);
+ }
+
// Create a hard link to a file (default) or directory (third argument is
// true). Throw std::system_error on failures.
//
- // Note that on Linix, FreeBSD and some other platforms the target can not
- // be a directory. While Windows support directories (via junktions), this
- // is currently not implemented.
+ // Note that on Linix, FreeBSD, Windows and some other platforms the target
+ // can not be a directory.
//
LIBBUTL_SYMEXPORT void
mkhardlink (const path& target, const path& link, bool dir = false);
diff --git a/tests/link/driver.cxx b/tests/link/driver.cxx
index 352cadd..da7e5b4 100644
--- a/tests/link/driver.cxx
+++ b/tests/link/driver.cxx
@@ -7,6 +7,7 @@
#ifndef __cpp_lib_modules
#include <set>
#include <utility> // pair
+#include <iostream> // cerr
#include <system_error>
#endif
@@ -15,12 +16,17 @@
#ifdef __cpp_modules
#ifdef __cpp_lib_modules
import std.core;
+import std.io;
#endif
import butl.path;
+import butl.path_io;
+import butl.utility;
import butl.fdstream;
import butl.filesystem;
#else
#include <libbutl/path.mxx>
+#include <libbutl/path-io.mxx>
+#include <libbutl/utility.mxx>
#include <libbutl/fdstream.mxx>
#include <libbutl/filesystem.mxx>
#endif
@@ -55,7 +61,6 @@ link_file (const path& target, const path& link, bool hard, bool check_content)
return s == text;
}
-#ifndef _WIN32
static bool
link_dir (const dir_path& target,
const dir_path& link,
@@ -69,12 +74,22 @@ link_dir (const dir_path& target,
else
mksymlink (target, link);
}
- catch (const system_error& e)
+ catch (const system_error&)
{
//cerr << e << endl;
return false;
}
+ {
+ auto pe (path_entry (link, false /* follow_symlinks */));
+ assert (pe.first && pe.second.type == entry_type::symlink);
+ }
+
+ {
+ auto pe (path_entry (link, true /* follow_symlinks */));
+ assert (!pe.first || pe.second.type == entry_type::directory);
+ }
+
if (!check_content)
return true;
@@ -90,7 +105,6 @@ link_dir (const dir_path& target,
return te == le;
}
-#endif
int
main ()
@@ -101,7 +115,16 @@ main ()
// faulty run) for the test files. Delete the directory only if the test
// succeeds to simplify the failure research.
//
- try_rmdir_r (td);
+ try
+ {
+ try_rmdir_r (td);
+ }
+ catch (const system_error& e)
+ {
+ cerr << "unable to remove " << td << ": " << e << endl;
+ return 1;
+ }
+
assert (try_mkdir (td) == mkdir_status::success);
// Prepare the target file.
@@ -131,27 +154,50 @@ main ()
// Create the file symlink using an unexistent file path.
//
assert (link_file (fp + "-a", td / path ("sa"), false, false));
+#endif
// Prepare the target directory.
//
dir_path dn ("dir");
dir_path dp (td / dn);
+
assert (try_mkdir (dp) == mkdir_status::success);
+
+ {
+ ofdstream ofs (dp / path ("f"));
+ ofs << text;
+ ofs.close ();
+ }
+
+#ifndef _WIN32
assert (link_file (fp, dp / path ("hlink"), true, true));
assert (link_file (fp, dp / path ("slink"), false, true));
+#endif
// Create the directory symlink using an absolute path.
//
- assert (link_dir (dp, td / dir_path ("dslink"), false, true));
+ dir_path ld (td / dir_path ("dslink"));
+ assert (link_dir (dp, ld, false, true));
- // Create the directory symlink using a relative path.
- //
- assert (link_dir (dn, td / dir_path ("rdslink"), false, true));
+ rmsymlink (ld);
+#ifndef _WIN32
// Create the directory symlink using an unexistent directory path.
//
assert (link_dir (dp / dir_path ("a"), td / dir_path ("dsa"), false, false));
+
+ // Create the directory symlink using a relative path.
+ //
+ assert (link_dir (dn, td / dir_path ("rdslink"), false, true));
#endif
- rmdir_r (td);
+ try
+ {
+ rmdir_r (td);
+ }
+ catch (const system_error& e)
+ {
+ cerr << "unable to remove " << td << ": " << e << endl;
+ return 1;
+ }
}