util.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. // Copyright (C) 2016 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. package fs
  7. import (
  8. "fmt"
  9. "os"
  10. "path/filepath"
  11. "runtime"
  12. "strings"
  13. )
  14. func ExpandTilde(path string) (string, error) {
  15. if path == "~" {
  16. return getHomeDir()
  17. }
  18. path = filepath.FromSlash(path)
  19. if !strings.HasPrefix(path, fmt.Sprintf("~%c", PathSeparator)) {
  20. return path, nil
  21. }
  22. home, err := getHomeDir()
  23. if err != nil {
  24. return "", err
  25. }
  26. return filepath.Join(home, path[2:]), nil
  27. }
  28. func getHomeDir() (string, error) {
  29. if runtime.GOOS == "windows" {
  30. // Legacy -- we prioritize this for historical reasons, whereas
  31. // os.UserHomeDir uses %USERPROFILE% always.
  32. home := filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath"))
  33. if home != "" {
  34. return home, nil
  35. }
  36. }
  37. return os.UserHomeDir()
  38. }
  39. var (
  40. windowsDisallowedCharacters = string([]rune{
  41. '<', '>', ':', '"', '|', '?', '*',
  42. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  43. 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
  44. 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
  45. 31,
  46. })
  47. windowsDisallowedNames = []string{"CON", "PRN", "AUX", "NUL",
  48. "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
  49. "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
  50. }
  51. )
  52. func WindowsInvalidFilename(name string) error {
  53. // None of the path components should end in space or period, or be a
  54. // reserved name.
  55. // (https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file)
  56. for _, part := range strings.Split(name, `\`) {
  57. if len(part) == 0 {
  58. continue
  59. }
  60. switch part[len(part)-1] {
  61. case ' ', '.':
  62. // Names ending in space or period are not valid.
  63. return errInvalidFilenameWindowsSpacePeriod
  64. }
  65. upperCased := strings.ToUpper(part)
  66. for _, disallowed := range windowsDisallowedNames {
  67. if upperCased == disallowed {
  68. return errInvalidFilenameWindowsReservedName
  69. }
  70. if strings.HasPrefix(upperCased, disallowed+".") {
  71. // nul.txt.jpg is also disallowed
  72. return errInvalidFilenameWindowsReservedName
  73. }
  74. }
  75. }
  76. // The path must not contain any disallowed characters
  77. if strings.ContainsAny(name, windowsDisallowedCharacters) {
  78. return errInvalidFilenameWindowsReservedChar
  79. }
  80. return nil
  81. }
  82. // IsParent compares paths purely lexicographically, meaning it returns false
  83. // if path and parent aren't both absolute or relative.
  84. func IsParent(path, parent string) bool {
  85. if parent == path {
  86. // Twice the same root on windows would not be caught at the end.
  87. return false
  88. }
  89. if filepath.IsAbs(path) != filepath.IsAbs(parent) {
  90. return false
  91. }
  92. if parent == "" || parent == "." {
  93. // The empty string is the parent of everything except the empty
  94. // string and ".". (Avoids panic in the last step.)
  95. return path != "" && path != "."
  96. }
  97. if parent == "/" {
  98. // The root is the parent of everything except itself, which would
  99. // not be caught below.
  100. return path != "/"
  101. }
  102. if parent[len(parent)-1] != PathSeparator {
  103. parent += string(PathSeparator)
  104. }
  105. return strings.HasPrefix(path, parent)
  106. }
  107. func CommonPrefix(first, second string) string {
  108. if filepath.IsAbs(first) != filepath.IsAbs(second) {
  109. // Whatever
  110. return ""
  111. }
  112. firstParts := strings.Split(filepath.Clean(first), string(PathSeparator))
  113. secondParts := strings.Split(filepath.Clean(second), string(PathSeparator))
  114. isAbs := filepath.IsAbs(first) && filepath.IsAbs(second)
  115. count := len(firstParts)
  116. if len(secondParts) < len(firstParts) {
  117. count = len(secondParts)
  118. }
  119. common := make([]string, 0, count)
  120. for i := 0; i < count; i++ {
  121. if firstParts[i] != secondParts[i] {
  122. break
  123. }
  124. common = append(common, firstParts[i])
  125. }
  126. if isAbs {
  127. if runtime.GOOS == "windows" && isVolumeNameOnly(common) {
  128. // Because strings.Split strips out path separators, if we're at the volume name, we end up without a separator
  129. // Wedge an empty element to be joined with.
  130. common = append(common, "")
  131. } else if len(common) == 1 {
  132. // If isAbs on non Windows, first element in both first and second is "", hence joining that returns nothing.
  133. return string(PathSeparator)
  134. }
  135. }
  136. // This should only be true on Windows when drive letters are different or when paths are relative.
  137. // In case of UNC paths we should end up with more than a single element hence joining is fine
  138. if len(common) == 0 {
  139. return ""
  140. }
  141. // This has to be strings.Join, because filepath.Join([]string{"", "", "?", "C:", "Audrius"}...) returns garbage
  142. result := strings.Join(common, string(PathSeparator))
  143. return filepath.Clean(result)
  144. }
  145. func isVolumeNameOnly(parts []string) bool {
  146. isNormalVolumeName := len(parts) == 1 && strings.HasSuffix(parts[0], ":")
  147. isUNCVolumeName := len(parts) == 4 && strings.HasSuffix(parts[3], ":")
  148. return isNormalVolumeName || isUNCVolumeName
  149. }