aboutsummaryrefslogtreecommitdiff
path: root/libbutl/filesystem.cxx
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 /libbutl/filesystem.cxx
parenteba3042910f063ae638a7e0134b79175978e2fca (diff)
Add support for directory symlinks on Windows
Diffstat (limited to 'libbutl/filesystem.cxx')
-rw-r--r--libbutl/filesystem.cxx193
1 files changed, 174 insertions, 19 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