Skip to content

Commit

Permalink
Implement proper renameFile on Windows
Browse files Browse the repository at this point in the history
Symlinks on Windows are implemented using NTFS reparse points. Reparse
points are also used when creating volume mounts on Windows. As far as
golang is concerned, mountpoints and symlinks look the same. This change
makes the distinction between the two and allows safely renaming
symlinks, while erring out for anything else that looks like a symlink.

Signed-off-by: Gabriel Adrian Samfira <[email protected]>
  • Loading branch information
gabriel-samfira committed Feb 9, 2023
1 parent fb43384 commit 4e7d36d
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 1 deletion.
3 changes: 2 additions & 1 deletion diskwriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ func (dw *DiskWriter) HandleChange(kind ChangeKind, p string, fi os.FileInfo, er
return errors.Wrapf(err, "failed to remove %s", destPath)
}
}
if err := os.Rename(newPath, destPath); err != nil {

if err := renameFile(newPath, destPath); err != nil {
return errors.Wrapf(err, "failed to rename %s to %s", newPath, destPath)
}
}
Expand Down
7 changes: 7 additions & 0 deletions diskwriter_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ func handleTarTypeBlockCharFifo(path string, stat *types.Stat) error {
}
return nil
}

func renameFile(src, dst string) error {
if err := os.Rename(src, dst); err != nil {
return errors.Wrapf(err, "failed to rename %s to %s", src, dst)
}
return nil
}
79 changes: 79 additions & 0 deletions diskwriter_windows.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
//go:build windows
// +build windows

package fsutil

import (
"fmt"
ioFS "io/fs"
"os"
"syscall"

"github.com/Microsoft/go-winio"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
)
Expand All @@ -16,3 +23,75 @@ func rewriteMetadata(p string, stat *types.Stat) error {
func handleTarTypeBlockCharFifo(path string, stat *types.Stat) error {
return errors.New("Not implemented on windows")
}

func getFileHandle(path string, info ioFS.FileInfo) (syscall.Handle, error) {
p, err := syscall.UTF16PtrFromString(path)
if err != nil {
return 0, errors.Wrap(err, "converting string to UTF-16")
}
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
if info.Mode()&os.ModeSymlink != 0 {
// Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink.
// See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted
attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT
}
h, err := syscall.CreateFile(p, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0)
if err != nil {
return 0, errors.Wrap(err, "getting file handle")
}
return h, nil
}

func readlink(path string, info ioFS.FileInfo) ([]byte, error) {
h, err := getFileHandle(path, info)
if err != nil {
return nil, errors.Wrap(err, "getting file handle")
}
defer syscall.CloseHandle(h)

rdbbuf := make([]byte, syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
var bytesReturned uint32
err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
if err != nil {
return nil, errors.Wrap(err, "sending I/O control command")
}
return rdbbuf[:bytesReturned], nil
}

func getReparsePoint(path string, info ioFS.FileInfo) (*winio.ReparsePoint, error) {
target, err := readlink(path, info)
if err != nil {
return nil, errors.Wrap(err, "fetching link")
}
rp, err := winio.DecodeReparsePoint(target)
if err != nil {
return nil, errors.Wrap(err, "decoding reparse point")
}
return rp, nil
}

func renameFile(src, dst string) error {
info, err := os.Lstat(dst)
if err != nil {
if !os.IsNotExist(err) {
return errors.Wrap(err, "getting file info")
}
}

if info != nil && info.Mode()&os.ModeSymlink != 0 {
dstInfoRp, err := getReparsePoint(dst, info)
if err != nil {
return errors.Wrap(err, "getting reparse point")
}
if dstInfoRp.IsMountPoint {
return fmt.Errorf("%s is a mount point", dst)
}
if err := os.Remove(dst); err != nil {
return errors.Wrapf(err, "removing %s", dst)
}
}
if err := os.Rename(src, dst); err != nil {
return errors.Wrapf(err, "failed to rename %s to %s", src, dst)
}
return nil
}

0 comments on commit 4e7d36d

Please sign in to comment.