symlink_windows.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. // Copyright (C) 2014 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at http://mozilla.org/MPL/2.0/.
  6. // +build windows
  7. package symlinks
  8. import (
  9. "os"
  10. "path/filepath"
  11. "github.com/syncthing/syncthing/lib/osutil"
  12. "github.com/syncthing/syncthing/lib/protocol"
  13. "syscall"
  14. "unicode/utf16"
  15. "unsafe"
  16. )
  17. const (
  18. Win32FsctlGetReparsePoint = 0x900a8
  19. Win32FileFlagOpenReparsePoint = 0x00200000
  20. Win32FileAttributeReparsePoint = 0x400
  21. Win32IOReparseTagSymlink = 0xA000000C
  22. Win32SymbolicLinkFlagDirectory = 0x1
  23. )
  24. var (
  25. modkernel32 = syscall.NewLazyDLL("kernel32.dll")
  26. procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
  27. procCreateSymbolicLink = modkernel32.NewProc("CreateSymbolicLinkW")
  28. Supported = false
  29. )
  30. func init() {
  31. defer func() {
  32. if err := recover(); err != nil {
  33. // Ensure that the supported flag is disabled when we hit an
  34. // error, even though it should already be. Also, silently swallow
  35. // the error since it's fine for a system not to support symlinks.
  36. Supported = false
  37. }
  38. }()
  39. // Needs administrator privileges.
  40. // Let's check that everything works.
  41. // This could be done more officially:
  42. // http://stackoverflow.com/questions/2094663/determine-if-windows-process-has-privilege-to-create-symbolic-link
  43. // But I don't want to define 10 more structs just to look this up.
  44. base := os.TempDir()
  45. path := filepath.Join(base, "symlinktest")
  46. defer os.Remove(path)
  47. err := Create(path, base, protocol.FlagDirectory)
  48. if err != nil {
  49. return
  50. }
  51. stat, err := osutil.Lstat(path)
  52. if err != nil || stat.Mode()&os.ModeSymlink == 0 {
  53. return
  54. }
  55. target, flags, err := Read(path)
  56. if err != nil || osutil.NativeFilename(target) != base || flags&protocol.FlagDirectory == 0 {
  57. return
  58. }
  59. Supported = true
  60. }
  61. type reparseData struct {
  62. reparseTag uint32
  63. reparseDataLength uint16
  64. reserved uint16
  65. substitueNameOffset uint16
  66. substitueNameLength uint16
  67. printNameOffset uint16
  68. printNameLength uint16
  69. flags uint32
  70. // substituteName - 264 widechars max = 528 bytes
  71. // printName - 260 widechars max = 520 bytes
  72. // = 1048 bytes total
  73. buffer [1048]uint16
  74. }
  75. func (r *reparseData) PrintName() string {
  76. // No clue why the offset and length is doubled...
  77. offset := r.printNameOffset / 2
  78. length := r.printNameLength / 2
  79. return string(utf16.Decode(r.buffer[offset : offset+length]))
  80. }
  81. func (r *reparseData) SubstituteName() string {
  82. // No clue why the offset and length is doubled...
  83. offset := r.substitueNameOffset / 2
  84. length := r.substitueNameLength / 2
  85. return string(utf16.Decode(r.buffer[offset : offset+length]))
  86. }
  87. func Read(path string) (string, TargetType, error) {
  88. ptr, err := syscall.UTF16PtrFromString(path)
  89. if err != nil {
  90. return "", TargetUnknown, err
  91. }
  92. handle, err := syscall.CreateFile(ptr, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|Win32FileFlagOpenReparsePoint, 0)
  93. if err != nil || handle == syscall.InvalidHandle {
  94. return "", TargetUnknown, err
  95. }
  96. defer syscall.Close(handle)
  97. var ret uint16
  98. var data reparseData
  99. r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), Win32FsctlGetReparsePoint, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0)
  100. if r1 == 0 {
  101. return "", TargetUnknown, err
  102. }
  103. tt := TargetUnknown
  104. if attr, err := syscall.GetFileAttributes(ptr); err == nil {
  105. if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
  106. tt = TargetDirectory
  107. } else {
  108. tt = TargetFile
  109. }
  110. }
  111. return osutil.NormalizedFilename(data.PrintName()), tt, nil
  112. }
  113. func Create(source, target string, tt TargetType) error {
  114. srcp, err := syscall.UTF16PtrFromString(source)
  115. if err != nil {
  116. return err
  117. }
  118. trgp, err := syscall.UTF16PtrFromString(osutil.NativeFilename(target))
  119. if err != nil {
  120. return err
  121. }
  122. // Sadly for Windows we need to specify the type of the symlink,
  123. // whether it's a directory symlink or a file symlink.
  124. // If the flags doesn't reveal the target type, try to evaluate it
  125. // ourselves, and worst case default to the symlink pointing to a file.
  126. mode := 0
  127. if tt == TargetUnknown {
  128. path := target
  129. if !filepath.IsAbs(target) {
  130. path = filepath.Join(filepath.Dir(source), target)
  131. }
  132. stat, err := os.Stat(path)
  133. if err == nil && stat.IsDir() {
  134. mode = Win32SymbolicLinkFlagDirectory
  135. }
  136. } else if tt == TargetDirectory {
  137. mode = Win32SymbolicLinkFlagDirectory
  138. }
  139. r0, _, err := syscall.Syscall(procCreateSymbolicLink.Addr(), 3, uintptr(unsafe.Pointer(srcp)), uintptr(unsafe.Pointer(trgp)), uintptr(mode))
  140. if r0 == 1 {
  141. return nil
  142. }
  143. return err
  144. }
  145. func ChangeType(path string, tt TargetType) error {
  146. target, exTt, err := Read(path)
  147. if err != nil {
  148. return err
  149. }
  150. // If it's the same type, nothing to do.
  151. if tt == exTt {
  152. return nil
  153. }
  154. // If the actual type is unknown, but the new type is file, nothing to do
  155. if exTt == TargetUnknown && tt != TargetDirectory {
  156. return nil
  157. }
  158. return osutil.InWritableDir(func(path string) error {
  159. // It should be a symlink as well hence no need to change permissions on
  160. // the file.
  161. os.Remove(path)
  162. return Create(path, target, tt)
  163. }, path)
  164. }