From 085493111005770ed33beeba07d317b6eba0c851 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 28 Apr 2018 17:58:36 +0300 Subject: Add support for directory symlinks on Windows --- libbutl/filesystem.cxx | 193 ++++++++++++++++++++++++++++++++++++++++++++----- libbutl/filesystem.mxx | 36 ++++++++- tests/link/driver.cxx | 64 +++++++++++++--- 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 // _find*(), _unlink(), _chmod() # include // _mkdir(), _rmdir() +# include // FSCTL_SET_REPARSE_POINT # include // _stat # include // _stat(), S_I* # include // _utime() +# include // 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 - 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 (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 (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 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 (-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 (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 (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 #include // pair +#include // cerr #include #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 +#include +#include #include #include #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; + } } -- cgit v1.1