pathutils.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2014 Jean-Marc Martins
  3. # Copyright © 2012-2017 Guillaume Ayoub
  4. # Copyright © 2017-2018 Unrud<unrud@outlook.com>
  5. #
  6. # This library is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  18. import contextlib
  19. import os
  20. import threading
  21. import posixpath # isort:skip
  22. if os.name == "nt":
  23. import ctypes
  24. import ctypes.wintypes
  25. import msvcrt
  26. LOCKFILE_EXCLUSIVE_LOCK = 2
  27. if ctypes.sizeof(ctypes.c_void_p) == 4:
  28. ULONG_PTR = ctypes.c_uint32
  29. else:
  30. ULONG_PTR = ctypes.c_uint64
  31. class Overlapped(ctypes.Structure):
  32. _fields_ = [
  33. ("internal", ULONG_PTR),
  34. ("internal_high", ULONG_PTR),
  35. ("offset", ctypes.wintypes.DWORD),
  36. ("offset_high", ctypes.wintypes.DWORD),
  37. ("h_event", ctypes.wintypes.HANDLE)]
  38. lock_file_ex = ctypes.windll.kernel32.LockFileEx
  39. lock_file_ex.argtypes = [
  40. ctypes.wintypes.HANDLE,
  41. ctypes.wintypes.DWORD,
  42. ctypes.wintypes.DWORD,
  43. ctypes.wintypes.DWORD,
  44. ctypes.wintypes.DWORD,
  45. ctypes.POINTER(Overlapped)]
  46. lock_file_ex.restype = ctypes.wintypes.BOOL
  47. unlock_file_ex = ctypes.windll.kernel32.UnlockFileEx
  48. unlock_file_ex.argtypes = [
  49. ctypes.wintypes.HANDLE,
  50. ctypes.wintypes.DWORD,
  51. ctypes.wintypes.DWORD,
  52. ctypes.wintypes.DWORD,
  53. ctypes.POINTER(Overlapped)]
  54. unlock_file_ex.restype = ctypes.wintypes.BOOL
  55. elif os.name == "posix":
  56. import fcntl
  57. class RwLock:
  58. """A readers-Writer lock that locks a file."""
  59. def __init__(self, path):
  60. self._path = path
  61. self._readers = 0
  62. self._writer = False
  63. self._lock = threading.Lock()
  64. @property
  65. def locked(self):
  66. with self._lock:
  67. if self._readers > 0:
  68. return "r"
  69. if self._writer:
  70. return "w"
  71. return ""
  72. @contextlib.contextmanager
  73. def acquire(self, mode):
  74. if mode not in "rw":
  75. raise ValueError("Invalid mode: %r" % mode)
  76. with open(self._path, "w+") as lock_file:
  77. if os.name == "nt":
  78. handle = msvcrt.get_osfhandle(lock_file.fileno())
  79. flags = LOCKFILE_EXCLUSIVE_LOCK if mode == "w" else 0
  80. overlapped = Overlapped()
  81. if not lock_file_ex(handle, flags, 0, 1, 0, overlapped):
  82. raise RuntimeError("Locking the storage failed: %s" %
  83. ctypes.FormatError())
  84. elif os.name == "posix":
  85. _cmd = fcntl.LOCK_EX if mode == "w" else fcntl.LOCK_SH
  86. try:
  87. fcntl.flock(lock_file.fileno(), _cmd)
  88. except OSError as e:
  89. raise RuntimeError("Locking the storage failed: %s" %
  90. e) from e
  91. else:
  92. raise RuntimeError("Locking the storage failed: "
  93. "Unsupported operating system")
  94. with self._lock:
  95. if self._writer or mode == "w" and self._readers != 0:
  96. raise RuntimeError("Locking the storage failed: "
  97. "Guarantees failed")
  98. if mode == "r":
  99. self._readers += 1
  100. else:
  101. self._writer = True
  102. try:
  103. yield
  104. finally:
  105. with self._lock:
  106. if mode == "r":
  107. self._readers -= 1
  108. self._writer = False
  109. def fsync(fd):
  110. if os.name == "posix" and hasattr(fcntl, "F_FULLFSYNC"):
  111. fcntl.fcntl(fd, fcntl.F_FULLFSYNC)
  112. else:
  113. os.fsync(fd)
  114. def strip_path(path):
  115. assert sanitize_path(path) == path
  116. return path.strip("/")
  117. def unstrip_path(stripped_path, trailing_slash=False):
  118. assert strip_path(sanitize_path(stripped_path)) == stripped_path
  119. assert stripped_path or trailing_slash
  120. path = "/%s" % stripped_path
  121. if trailing_slash and not path.endswith("/"):
  122. path += "/"
  123. return path
  124. def sanitize_path(path):
  125. """Make path absolute with leading slash to prevent access to other data.
  126. Preserve potential trailing slash.
  127. """
  128. trailing_slash = "/" if path.endswith("/") else ""
  129. path = posixpath.normpath(path)
  130. new_path = "/"
  131. for part in path.split("/"):
  132. if not is_safe_path_component(part):
  133. continue
  134. new_path = posixpath.join(new_path, part)
  135. trailing_slash = "" if new_path.endswith("/") else trailing_slash
  136. return new_path + trailing_slash
  137. def is_safe_path_component(path):
  138. """Check if path is a single component of a path.
  139. Check that the path is safe to join too.
  140. """
  141. return path and "/" not in path and path not in (".", "..")
  142. def is_safe_filesystem_path_component(path):
  143. """Check if path is a single component of a local and posix filesystem
  144. path.
  145. Check that the path is safe to join too.
  146. """
  147. return (
  148. path and not os.path.splitdrive(path)[0] and
  149. not os.path.split(path)[0] and path not in (os.curdir, os.pardir) and
  150. not path.startswith(".") and not path.endswith("~") and
  151. is_safe_path_component(path))
  152. def path_to_filesystem(root, sane_path):
  153. """Convert `sane_path` to a local filesystem path relative to `root`.
  154. `root` must be a secure filesystem path, it will be prepend to the path.
  155. `sane_path` must be a sanitized path without leading or trailing ``/``.
  156. Conversion of `sane_path` is done in a secure manner,
  157. or raises ``ValueError``.
  158. """
  159. assert sane_path == strip_path(sanitize_path(sane_path))
  160. safe_path = root
  161. parts = sane_path.split("/") if sane_path else []
  162. for part in parts:
  163. if not is_safe_filesystem_path_component(part):
  164. raise UnsafePathError(part)
  165. safe_path_parent = safe_path
  166. safe_path = os.path.join(safe_path, part)
  167. # Check for conflicting files (e.g. case-insensitive file systems
  168. # or short names on Windows file systems)
  169. if (os.path.lexists(safe_path) and
  170. part not in (e.name for e in
  171. os.scandir(safe_path_parent))):
  172. raise CollidingPathError(part)
  173. return safe_path
  174. class UnsafePathError(ValueError):
  175. def __init__(self, path):
  176. message = "Can't translate name safely to filesystem: %r" % path
  177. super().__init__(message)
  178. class CollidingPathError(ValueError):
  179. def __init__(self, path):
  180. message = "File name collision: %r" % path
  181. super().__init__(message)
  182. def name_from_path(path, collection):
  183. """Return Radicale item name from ``path``."""
  184. assert sanitize_path(path) == path
  185. start = unstrip_path(collection.path, True)
  186. if not (path + "/").startswith(start):
  187. raise ValueError("%r doesn't start with %r" % (path, start))
  188. name = path[len(start):]
  189. if name and not is_safe_path_component(name):
  190. raise ValueError("%r is not a component in collection %r" %
  191. (name, collection.path))
  192. return name