phobos icon indicating copy to clipboard operation
phobos copied to clipboard

Adds symlink to windows

Open MrcSnm opened this issue 10 months ago • 8 comments

Feel free to improve however you see fit. I found it a little lacking to not have Windows support. I can't understand how to write in phobos way, but the base implementation is there.

MrcSnm avatar Aug 09 '23 00:08 MrcSnm

Thanks for your pull request and interest in making D better, @MrcSnm! We are looking forward to reviewing it, and you should be hearing from a maintainer soon. Please verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the coverage diff by visiting the details link of the codecov check)
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

Please see CONTRIBUTING.md for more information.


If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment.

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + phobos#8794"

dlang-bot avatar Aug 09 '23 00:08 dlang-bot

The doc says $(BLUE This function is POSIX-Only.) so that line should be perhaps be changed to: POSIX and Windows Only

ryuukk avatar Aug 09 '23 03:08 ryuukk

OK, here is a draft of a start:

version (StdDdoc)
{
    /++
        $(BLUE This function is Windows-Only.)

        Creates a Windows symbolic link.

        "Developer Mode" may need to be first enabled on the local
        machine before unprivileged processes are allowed to create
        symbolic links.

        Note that symbolic links on Windows have slightly different
        semantics than those on POSIX.  For example, it must be known
        at creation time whether the symbolic link points / will point
        to a file or directory.  To delete a Windows symbolic link,
        `rmdir` must be used instead of `remove` if it was created as
        pointing at a directory.

        Params:
            original = The location that is being linked. This is the
                target path that's stored in the reparse point.  The
                path may be relative, in which case it is relative to
                the symbolic link's parent directory.

            link = The location of the junction to create. A relative
                path is relative to the current working directory.

        Throws:
            $(LREF WindowsException) on error.
      +/
    // TODO: isDir(original) is not correct when original is a relative path
    void createWindowsSymlink(in char[] original, in char[] link, bool originalIsDir = isDir(original));

    /++
        $(BLUE This function is Windows-Only.)

        Creates a Windows filesystem junction.

        Junctions are similar to symbolic links, but cannot be
        relative and cannot point to files; on the other hand, they
        can be created even without special privileges or "Developer
        Mode".

        Params:
            original = The location that is being linked.  This is the
                target path that's stored in the reparse point.  A
                relative path is first resolved to an absolute path.

            link = The location of the junction to create. A relative
                path is relative to the current working directory.

        Throws:
            $(LREF WindowsException) on error.
      +/
    void createJunction(in char[] original, in char[] link);

}
else
version (Windows)
{
    /// Create an NTFS reparse point (junction or symbolic link).
    private void createReparsePoint
        (string reparseBufferName, string extraInitialization, string reparseTagName)
        (in char[] target, in char[] print, in char[] link)
    {
        import core.sys.windows.winbase;
        import core.sys.windows.windef;
        import core.sys.windows.winioctl;

        enum SYMLINK_FLAG_RELATIVE = 1;

        HANDLE hLink = CreateFileW(
            link.toUTF16z(),
            GENERIC_READ | GENERIC_WRITE,
            0, null,
            OPEN_EXISTING,
            FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
            null);
        wenforce(hLink && hLink != INVALID_HANDLE_VALUE, "CreateFileW");
        scope(exit) CloseHandle(hLink);

        enum pathOffset =
            mixin(q{REPARSE_DATA_BUFFER.} ~ reparseBufferName)            .offsetof +
            mixin(q{REPARSE_DATA_BUFFER.} ~ reparseBufferName)._PathBuffer.offsetof;

        auto targetW = target.toUTF16();
        auto printW  = print .toUTF16();

        // Despite MSDN, two NUL-terminating characters are needed, one for each string.

        auto pathBufferSize = targetW.length + 1 + printW.length + 1; // in chars
        auto buf = new ubyte[pathOffset + pathBufferSize * WCHAR.sizeof];
        auto r = cast(REPARSE_DATA_BUFFER*)buf.ptr;

        r.ReparseTag = mixin(reparseTagName);
        r.ReparseDataLength = to!WORD(buf.length - mixin(q{r.} ~ reparseBufferName).offsetof);

        auto pathBuffer = mixin(q{r.} ~ reparseBufferName).PathBuffer;
        auto p = pathBuffer;

        mixin(q{r.} ~ reparseBufferName).SubstituteNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof);
        mixin(q{r.} ~ reparseBufferName).SubstituteNameLength = to!WORD(targetW.length * WCHAR.sizeof);
        p[0..targetW.length] = targetW;
        p += targetW.length;
        *p++ = 0;

        mixin(q{r.} ~ reparseBufferName).PrintNameOffset      = to!WORD((p-pathBuffer) * WCHAR.sizeof);
        mixin(q{r.} ~ reparseBufferName).PrintNameLength      = to!WORD(printW .length * WCHAR.sizeof);
        p[0..printW.length] = printW;
        p += printW.length;
        *p++ = 0;

        assert(p-pathBuffer == pathBufferSize);

        mixin(extraInitialization);

        DWORD dwRet; // Needed despite MSDN
        DeviceIoControl(hLink, FSCTL_SET_REPARSE_POINT, buf.ptr, buf.length.to!DWORD(), null, 0, &dwRet, null).wenforce("DeviceIoControl");
    }

    void createWindowsSymlink(in char[] original, in char[] link, bool originalIsDir = isDir(original))
    {
        import core.sys.windows.winnt;

        if (originalIsDir)
            mkdir(link);
        else
            write(link, "");

        scope (failure)
            if (originalIsDir)
                rmdir(link);
            else
                remove(link);

        createReparsePoint!(
            q{SymbolicLinkReparseBuffer},
            q{r.SymbolicLinkReparseBuffer.Flags = link.isAbsolute() ? 0 : SYMLINK_FLAG_RELATIVE;},
            q{IO_REPARSE_TAG_SYMLINK}
        )(original, original, link);
    }

    void createJunction(in char[] original, in char[] link)
    {
        mkdir(link);
        scope(failure) rmdir(link);

        auto target = `\??\` ~ (cast(string)original).absolutePath((cast(string)link.dirName).absolutePath).buildNormalizedPath;
        if (target[$-1] != '\\')
            target ~= '\\';

        createReparsePoint!(
            q{MountPointReparseBuffer},
            q{},
            q{IO_REPARSE_TAG_MOUNT_POINT}
        )(target, null, link);
    }
}

CyberShadow avatar Aug 11 '23 09:08 CyberShadow

OK, here is a draft of a start:

I have changed to use your approach, also added the enum WindowsSymlinkHint for making it clearer on the purpose. I would like to ask what was your intent with isDir(original) not being enough though.

MrcSnm avatar Aug 15 '23 01:08 MrcSnm

Great, thanks!

I would like to ask what was your intent with isDir(original) not being enough though.

If original is relative, it needs to be relative to the parent directory of the link object, not of the program's current working directory.

CyberShadow avatar Aug 15 '23 03:08 CyberShadow

@atilaneves What do you think about adding these two Windows-only functions for symlinks?

CyberShadow avatar Aug 15 '23 03:08 CyberShadow

I don't think OS-specific code like this belongs in Phobos.

atilaneves avatar Aug 15 '23 10:08 atilaneves

I don't think OS-specific code like this belongs in Phobos.

symlink is OS-specific code to Posix, std.windows.registry is also. Having them in the std is incredibly useful. Also phobos don't give other ways to create symlinks for Windows even though this is a pretty common operation, by being common IMO it already deserves that.

MrcSnm avatar Aug 15 '23 10:08 MrcSnm