util.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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. "errors"
  9. "fmt"
  10. "os"
  11. "path/filepath"
  12. "runtime"
  13. "strings"
  14. )
  15. var errNoHome = errors.New("no home directory found - set $HOME (or the platform equivalent)")
  16. func ExpandTilde(path string) (string, error) {
  17. if path == "~" {
  18. return getHomeDir()
  19. }
  20. path = filepath.FromSlash(path)
  21. if !strings.HasPrefix(path, fmt.Sprintf("~%c", PathSeparator)) {
  22. return path, nil
  23. }
  24. home, err := getHomeDir()
  25. if err != nil {
  26. return "", err
  27. }
  28. return filepath.Join(home, path[2:]), nil
  29. }
  30. func getHomeDir() (string, error) {
  31. var home string
  32. switch runtime.GOOS {
  33. case "windows":
  34. home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath"))
  35. if home == "" {
  36. home = os.Getenv("UserProfile")
  37. }
  38. default:
  39. home = os.Getenv("HOME")
  40. }
  41. if home == "" {
  42. return "", errNoHome
  43. }
  44. return home, nil
  45. }
  46. var windowsDisallowedCharacters = string([]rune{
  47. '<', '>', ':', '"', '|', '?', '*',
  48. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  49. 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
  50. 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
  51. 31,
  52. })
  53. func WindowsInvalidFilename(name string) bool {
  54. // None of the path components should end in space
  55. for _, part := range strings.Split(name, `\`) {
  56. if len(part) == 0 {
  57. continue
  58. }
  59. if part[len(part)-1] == ' ' {
  60. // Names ending in space are not valid.
  61. return true
  62. }
  63. }
  64. // The path must not contain any disallowed characters
  65. return strings.ContainsAny(name, windowsDisallowedCharacters)
  66. }
  67. // IsParent compares paths purely lexicographically, meaning it returns false
  68. // if path and parent aren't both absolute or relative.
  69. func IsParent(path, parent string) bool {
  70. if parent == path {
  71. // Twice the same root on windows would not be caught at the end.
  72. return false
  73. }
  74. if filepath.IsAbs(path) != filepath.IsAbs(parent) {
  75. return false
  76. }
  77. if parent == "" || parent == "." {
  78. // The empty string is the parent of everything except the empty
  79. // string and ".". (Avoids panic in the last step.)
  80. return path != "" && path != "."
  81. }
  82. if parent == "/" {
  83. // The root is the parent of everything except itself, which would
  84. // not be caught below.
  85. return path != "/"
  86. }
  87. if parent[len(parent)-1] != PathSeparator {
  88. parent += string(PathSeparator)
  89. }
  90. return strings.HasPrefix(path, parent)
  91. }
  92. func CommonPrefix(first, second string) string {
  93. if filepath.IsAbs(first) != filepath.IsAbs(second) {
  94. // Whatever
  95. return ""
  96. }
  97. firstParts := strings.Split(filepath.Clean(first), string(PathSeparator))
  98. secondParts := strings.Split(filepath.Clean(second), string(PathSeparator))
  99. isAbs := filepath.IsAbs(first) && filepath.IsAbs(second)
  100. count := len(firstParts)
  101. if len(secondParts) < len(firstParts) {
  102. count = len(secondParts)
  103. }
  104. common := make([]string, 0, count)
  105. for i := 0; i < count; i++ {
  106. if firstParts[i] != secondParts[i] {
  107. break
  108. }
  109. common = append(common, firstParts[i])
  110. }
  111. if isAbs {
  112. if runtime.GOOS == "windows" && isVolumeNameOnly(common) {
  113. // Because strings.Split strips out path separators, if we're at the volume name, we end up without a separator
  114. // Wedge an empty element to be joined with.
  115. common = append(common, "")
  116. } else if len(common) == 1 {
  117. // If isAbs on non Windows, first element in both first and second is "", hence joining that returns nothing.
  118. return string(PathSeparator)
  119. }
  120. }
  121. // This should only be true on Windows when drive letters are different or when paths are relative.
  122. // In case of UNC paths we should end up with more than a single element hence joining is fine
  123. if len(common) == 0 {
  124. return ""
  125. }
  126. // This has to be strings.Join, because filepath.Join([]string{"", "", "?", "C:", "Audrius"}...) returns garbage
  127. result := strings.Join(common, string(PathSeparator))
  128. return filepath.Clean(result)
  129. }
  130. func isVolumeNameOnly(parts []string) bool {
  131. isNormalVolumeName := len(parts) == 1 && strings.HasSuffix(parts[0], ":")
  132. isUNCVolumeName := len(parts) == 4 && strings.HasSuffix(parts[3], ":")
  133. return isNormalVolumeName || isUNCVolumeName
  134. }