basicfs_windows.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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 https://mozilla.org/MPL/2.0/.
  6. //go:build windows
  7. // +build windows
  8. package fs
  9. import (
  10. "bytes"
  11. "errors"
  12. "os"
  13. "path/filepath"
  14. "strings"
  15. "syscall"
  16. "unsafe"
  17. "golang.org/x/sys/windows"
  18. )
  19. var errNotSupported = errors.New("symlinks not supported")
  20. func (BasicFilesystem) SymlinksSupported() bool {
  21. return false
  22. }
  23. func (BasicFilesystem) ReadSymlink(path string) (string, error) {
  24. return "", errNotSupported
  25. }
  26. func (BasicFilesystem) CreateSymlink(target, name string) error {
  27. return errNotSupported
  28. }
  29. // Required due to https://github.com/golang/go/issues/10900
  30. func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
  31. // Fast path: if we can tell whether path is a directory or file, stop with success or error.
  32. dir, err := os.Stat(path)
  33. if err == nil {
  34. if dir.IsDir() {
  35. return nil
  36. }
  37. return &os.PathError{
  38. Op: "mkdir",
  39. Path: path,
  40. Err: syscall.ENOTDIR,
  41. }
  42. }
  43. // Slow path: make sure parent exists and then call Mkdir for path.
  44. i := len(path)
  45. for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator.
  46. i--
  47. }
  48. j := i
  49. for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element.
  50. j--
  51. }
  52. if j > 1 {
  53. // Create parent
  54. parent := path[0 : j-1]
  55. if parent != filepath.VolumeName(parent) {
  56. err = f.mkdirAll(parent, perm)
  57. if err != nil {
  58. return err
  59. }
  60. }
  61. }
  62. // Parent now exists; invoke Mkdir and use its result.
  63. err = os.Mkdir(path, perm)
  64. if err != nil {
  65. // Handle arguments like "foo/." by
  66. // double-checking that directory doesn't exist.
  67. dir, err1 := os.Lstat(path)
  68. if err1 == nil && dir.IsDir() {
  69. return nil
  70. }
  71. return err
  72. }
  73. return nil
  74. }
  75. func (f *BasicFilesystem) Unhide(name string) error {
  76. name, err := f.rooted(name)
  77. if err != nil {
  78. return err
  79. }
  80. p, err := syscall.UTF16PtrFromString(name)
  81. if err != nil {
  82. return err
  83. }
  84. attrs, err := syscall.GetFileAttributes(p)
  85. if err != nil {
  86. return err
  87. }
  88. attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
  89. return syscall.SetFileAttributes(p, attrs)
  90. }
  91. func (f *BasicFilesystem) Hide(name string) error {
  92. name, err := f.rooted(name)
  93. if err != nil {
  94. return err
  95. }
  96. p, err := syscall.UTF16PtrFromString(name)
  97. if err != nil {
  98. return err
  99. }
  100. attrs, err := syscall.GetFileAttributes(p)
  101. if err != nil {
  102. return err
  103. }
  104. attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
  105. return syscall.SetFileAttributes(p, attrs)
  106. }
  107. func (f *BasicFilesystem) Roots() ([]string, error) {
  108. kernel32, err := syscall.LoadDLL("kernel32.dll")
  109. if err != nil {
  110. return nil, err
  111. }
  112. getLogicalDriveStringsHandle, err := kernel32.FindProc("GetLogicalDriveStringsA")
  113. if err != nil {
  114. return nil, err
  115. }
  116. buffer := [1024]byte{}
  117. bufferSize := uint32(len(buffer))
  118. hr, _, _ := getLogicalDriveStringsHandle.Call(uintptr(unsafe.Pointer(&bufferSize)), uintptr(unsafe.Pointer(&buffer)))
  119. if hr == 0 {
  120. return nil, errors.New("syscall failed")
  121. }
  122. var drives []string
  123. parts := bytes.Split(buffer[:], []byte{0})
  124. for _, part := range parts {
  125. if len(part) == 0 {
  126. break
  127. }
  128. drives = append(drives, string(part))
  129. }
  130. return drives, nil
  131. }
  132. func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
  133. name, err := f.rooted(name)
  134. if err != nil {
  135. return err
  136. }
  137. hdl, err := windows.Open(name, windows.O_WRONLY, 0)
  138. if err != nil {
  139. return err
  140. }
  141. defer windows.Close(hdl)
  142. // Depending on whether we got an uid or a gid, we need to set the
  143. // appropriate flag and parse the corresponding SID. The one we're not
  144. // setting remains nil, which is what we want in the call to
  145. // SetSecurityInfo.
  146. var si windows.SECURITY_INFORMATION
  147. var ownerSID, groupSID *syscall.SID
  148. if uid != "" {
  149. ownerSID, err = syscall.StringToSid(uid)
  150. if err == nil {
  151. si |= windows.OWNER_SECURITY_INFORMATION
  152. }
  153. } else if gid != "" {
  154. groupSID, err = syscall.StringToSid(uid)
  155. if err == nil {
  156. si |= windows.GROUP_SECURITY_INFORMATION
  157. }
  158. } else {
  159. return errors.New("neither uid nor gid specified")
  160. }
  161. return windows.SetSecurityInfo(hdl, windows.SE_FILE_OBJECT, si, (*windows.SID)(ownerSID), (*windows.SID)(groupSID), nil, nil)
  162. }
  163. func (f *BasicFilesystem) Remove(name string) error {
  164. name, err := f.rooted(name)
  165. if err != nil {
  166. return err
  167. }
  168. err = os.Remove(name)
  169. if os.IsPermission(err) {
  170. // Try to remove the read-only attribute and try again
  171. if os.Chmod(name, 0o600) == nil {
  172. err = os.Remove(name)
  173. }
  174. }
  175. return err
  176. }
  177. // unrootedChecked returns the path relative to the folder root (same as
  178. // unrooted) or an error if the given path is not a subpath and handles the
  179. // special case when the given path is the folder root without a trailing
  180. // pathseparator.
  181. func (f *BasicFilesystem) unrootedChecked(absPath string, roots []string) (string, error) {
  182. absPath = f.resolveWin83(absPath)
  183. lowerAbsPath := UnicodeLowercaseNormalized(absPath)
  184. for _, root := range roots {
  185. lowerRoot := UnicodeLowercaseNormalized(root)
  186. if lowerAbsPath+string(PathSeparator) == lowerRoot {
  187. return ".", nil
  188. }
  189. if strings.HasPrefix(lowerAbsPath, lowerRoot) {
  190. return rel(absPath, root), nil
  191. }
  192. }
  193. return "", f.newErrWatchEventOutsideRoot(lowerAbsPath, roots)
  194. }
  195. func rel(path, prefix string) string {
  196. lowerRel := strings.TrimPrefix(strings.TrimPrefix(UnicodeLowercaseNormalized(path), UnicodeLowercaseNormalized(prefix)), string(PathSeparator))
  197. return path[len(path)-len(lowerRel):]
  198. }
  199. func (f *BasicFilesystem) resolveWin83(absPath string) string {
  200. if !isMaybeWin83(absPath) {
  201. return absPath
  202. }
  203. if in, err := syscall.UTF16FromString(absPath); err == nil {
  204. out := make([]uint16, 4*len(absPath)) // *2 for UTF16 and *2 to double path length
  205. if n, err := syscall.GetLongPathName(&in[0], &out[0], uint32(len(out))); err == nil {
  206. if n <= uint32(len(out)) {
  207. return syscall.UTF16ToString(out[:n])
  208. }
  209. out = make([]uint16, n)
  210. if _, err = syscall.GetLongPathName(&in[0], &out[0], n); err == nil {
  211. return syscall.UTF16ToString(out)
  212. }
  213. }
  214. }
  215. // Failed getting the long path. Return the part of the path which is
  216. // already a long path.
  217. lowerRoot := UnicodeLowercaseNormalized(f.root)
  218. for absPath = filepath.Dir(absPath); strings.HasPrefix(UnicodeLowercaseNormalized(absPath), lowerRoot); absPath = filepath.Dir(absPath) {
  219. if !isMaybeWin83(absPath) {
  220. return absPath
  221. }
  222. }
  223. return f.root
  224. }
  225. func isMaybeWin83(absPath string) bool {
  226. if !strings.Contains(absPath, "~") {
  227. return false
  228. }
  229. if strings.Contains(filepath.Dir(absPath), "~") {
  230. return true
  231. }
  232. return strings.Contains(strings.TrimPrefix(filepath.Base(absPath), WindowsTempPrefix), "~")
  233. }
  234. func getFinalPathName(in string) (string, error) {
  235. // Return the normalized path
  236. // Wrap the call to GetFinalPathNameByHandleW
  237. // The string returned by this function uses the \?\ syntax
  238. // Implies GetFullPathName + GetLongPathName
  239. kernel32, err := syscall.LoadDLL("kernel32.dll")
  240. if err != nil {
  241. return "", err
  242. }
  243. GetFinalPathNameByHandleW, err := kernel32.FindProc("GetFinalPathNameByHandleW")
  244. // https://github.com/golang/go/blob/ff048033e4304898245d843e79ed1a0897006c6d/src/internal/syscall/windows/syscall_windows.go#L303
  245. if err != nil {
  246. return "", err
  247. }
  248. inPath, err := syscall.UTF16PtrFromString(in)
  249. if err != nil {
  250. return "", err
  251. }
  252. // Get a file handler
  253. h, err := syscall.CreateFile(inPath,
  254. syscall.GENERIC_READ,
  255. syscall.FILE_SHARE_READ,
  256. nil,
  257. syscall.OPEN_EXISTING,
  258. uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS),
  259. 0)
  260. if err != nil {
  261. return "", err
  262. }
  263. defer syscall.CloseHandle(h)
  264. // Call GetFinalPathNameByHandleW
  265. var VOLUME_NAME_DOS uint32 = 0x0 // not yet defined in syscall
  266. var bufSize uint32 = syscall.MAX_PATH // 260
  267. for i := 0; i < 2; i++ {
  268. buf := make([]uint16, bufSize)
  269. var ret uintptr
  270. ret, _, err = GetFinalPathNameByHandleW.Call(
  271. uintptr(h), // HANDLE hFile
  272. uintptr(unsafe.Pointer(&buf[0])), // LPWSTR lpszFilePath
  273. uintptr(bufSize), // DWORD cchFilePath
  274. uintptr(VOLUME_NAME_DOS), // DWORD dwFlags
  275. )
  276. // The returned value is the actual length of the norm path
  277. // After Win 10 build 1607, MAX_PATH limitations have been removed
  278. // so it is necessary to check newBufSize
  279. newBufSize := uint32(ret) + 1
  280. if ret == 0 || newBufSize > bufSize*100 {
  281. break
  282. }
  283. if newBufSize <= bufSize {
  284. return syscall.UTF16ToString(buf), nil
  285. }
  286. bufSize = newBufSize
  287. }
  288. return "", err
  289. }
  290. func evalSymlinks(in string) (string, error) {
  291. out, err := filepath.EvalSymlinks(in)
  292. if err != nil && strings.HasPrefix(in, `\\?\`) {
  293. // Try again without the `\\?\` prefix
  294. out, err = filepath.EvalSymlinks(in[4:])
  295. }
  296. if err != nil {
  297. // Try to get a normalized path from Win-API
  298. var err1 error
  299. out, err1 = getFinalPathName(in)
  300. if err1 != nil {
  301. return "", err // return the prior error
  302. }
  303. // Trim UNC prefix, equivalent to
  304. // https://github.com/golang/go/blob/2396101e0590cb7d77556924249c26af0ccd9eff/src/os/file_windows.go#L470
  305. if strings.HasPrefix(out, `\\?\UNC\`) {
  306. out = `\` + out[7:] // path like \\server\share\...
  307. } else {
  308. out = strings.TrimPrefix(out, `\\?\`)
  309. }
  310. }
  311. return longFilenameSupport(out), nil
  312. }
  313. // watchPaths adjust the folder root for use with the notify backend and the
  314. // corresponding absolute path to be passed to notify to watch name.
  315. func (f *BasicFilesystem) watchPaths(name string) (string, []string, error) {
  316. root, err := evalSymlinks(f.root)
  317. if err != nil {
  318. return "", nil, err
  319. }
  320. // Remove `\\?\` prefix if the path is just a drive letter as a dirty
  321. // fix for https://github.com/syncthing/syncthing/issues/5578
  322. if filepath.Clean(name) == "." && len(root) <= 7 && len(root) > 4 && root[:4] == `\\?\` {
  323. root = root[4:]
  324. }
  325. absName, err := rooted(name, root)
  326. if err != nil {
  327. return "", nil, err
  328. }
  329. roots := []string{f.resolveWin83(root)}
  330. absName = f.resolveWin83(absName)
  331. // Events returned from fs watching are all over the place, so allow
  332. // both the user's input and the result of "canonicalizing" the path.
  333. if roots[0] != f.root {
  334. roots = append(roots, f.root)
  335. }
  336. return filepath.Join(absName, "..."), roots, nil
  337. }