pathutils.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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. """
  19. Helper functions for working with the file system.
  20. """
  21. import contextlib
  22. import os
  23. import posixpath
  24. import threading
  25. if os.name == "nt":
  26. import ctypes
  27. import ctypes.wintypes
  28. import msvcrt
  29. LOCKFILE_EXCLUSIVE_LOCK = 2
  30. if ctypes.sizeof(ctypes.c_void_p) == 4:
  31. ULONG_PTR = ctypes.c_uint32
  32. else:
  33. ULONG_PTR = ctypes.c_uint64
  34. class Overlapped(ctypes.Structure):
  35. _fields_ = [
  36. ("internal", ULONG_PTR),
  37. ("internal_high", ULONG_PTR),
  38. ("offset", ctypes.wintypes.DWORD),
  39. ("offset_high", ctypes.wintypes.DWORD),
  40. ("h_event", ctypes.wintypes.HANDLE)]
  41. kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
  42. lock_file_ex = kernel32.LockFileEx
  43. lock_file_ex.argtypes = [
  44. ctypes.wintypes.HANDLE,
  45. ctypes.wintypes.DWORD,
  46. ctypes.wintypes.DWORD,
  47. ctypes.wintypes.DWORD,
  48. ctypes.wintypes.DWORD,
  49. ctypes.POINTER(Overlapped)]
  50. lock_file_ex.restype = ctypes.wintypes.BOOL
  51. unlock_file_ex = kernel32.UnlockFileEx
  52. unlock_file_ex.argtypes = [
  53. ctypes.wintypes.HANDLE,
  54. ctypes.wintypes.DWORD,
  55. ctypes.wintypes.DWORD,
  56. ctypes.wintypes.DWORD,
  57. ctypes.POINTER(Overlapped)]
  58. unlock_file_ex.restype = ctypes.wintypes.BOOL
  59. elif os.name == "posix":
  60. import fcntl
  61. class RwLock:
  62. """A readers-Writer lock that locks a file."""
  63. def __init__(self, path):
  64. self._path = path
  65. self._readers = 0
  66. self._writer = False
  67. self._lock = threading.Lock()
  68. @property
  69. def locked(self):
  70. with self._lock:
  71. if self._readers > 0:
  72. return "r"
  73. if self._writer:
  74. return "w"
  75. return ""
  76. @contextlib.contextmanager
  77. def acquire(self, mode):
  78. if mode not in "rw":
  79. raise ValueError("Invalid mode: %r" % mode)
  80. with open(self._path, "w+") as lock_file:
  81. if os.name == "nt":
  82. handle = msvcrt.get_osfhandle(lock_file.fileno())
  83. flags = LOCKFILE_EXCLUSIVE_LOCK if mode == "w" else 0
  84. overlapped = Overlapped()
  85. try:
  86. if not lock_file_ex(handle, flags, 0, 1, 0, overlapped):
  87. raise ctypes.WinError()
  88. except OSError as e:
  89. raise RuntimeError("Locking the storage failed: %s" %
  90. e) from e
  91. elif os.name == "posix":
  92. _cmd = fcntl.LOCK_EX if mode == "w" else fcntl.LOCK_SH
  93. try:
  94. fcntl.flock(lock_file.fileno(), _cmd)
  95. except OSError as e:
  96. raise RuntimeError("Locking the storage failed: %s" %
  97. e) from e
  98. else:
  99. raise RuntimeError("Locking the storage failed: "
  100. "Unsupported operating system")
  101. with self._lock:
  102. if self._writer or mode == "w" and self._readers != 0:
  103. raise RuntimeError("Locking the storage failed: "
  104. "Guarantees failed")
  105. if mode == "r":
  106. self._readers += 1
  107. else:
  108. self._writer = True
  109. try:
  110. yield
  111. finally:
  112. with self._lock:
  113. if mode == "r":
  114. self._readers -= 1
  115. self._writer = False
  116. def fsync(fd):
  117. if os.name == "posix" and hasattr(fcntl, "F_FULLFSYNC"):
  118. fcntl.fcntl(fd, fcntl.F_FULLFSYNC)
  119. else:
  120. os.fsync(fd)
  121. def strip_path(path):
  122. assert sanitize_path(path) == path
  123. return path.strip("/")
  124. def unstrip_path(stripped_path, trailing_slash=False):
  125. assert strip_path(sanitize_path(stripped_path)) == stripped_path
  126. assert stripped_path or trailing_slash
  127. path = "/%s" % stripped_path
  128. if trailing_slash and not path.endswith("/"):
  129. path += "/"
  130. return path
  131. def sanitize_path(path):
  132. """Make path absolute with leading slash to prevent access to other data.
  133. Preserve potential trailing slash.
  134. """
  135. trailing_slash = "/" if path.endswith("/") else ""
  136. path = posixpath.normpath(path)
  137. new_path = "/"
  138. for part in path.split("/"):
  139. if not is_safe_path_component(part):
  140. continue
  141. new_path = posixpath.join(new_path, part)
  142. trailing_slash = "" if new_path.endswith("/") else trailing_slash
  143. return new_path + trailing_slash
  144. def is_safe_path_component(path):
  145. """Check if path is a single component of a path.
  146. Check that the path is safe to join too.
  147. """
  148. return path and "/" not in path and path not in (".", "..")
  149. def is_safe_filesystem_path_component(path):
  150. """Check if path is a single component of a local and posix filesystem
  151. path.
  152. Check that the path is safe to join too.
  153. """
  154. return (
  155. path and not os.path.splitdrive(path)[0] and
  156. not os.path.split(path)[0] and path not in (os.curdir, os.pardir) and
  157. not path.startswith(".") and not path.endswith("~") and
  158. is_safe_path_component(path))
  159. def path_to_filesystem(root, sane_path):
  160. """Convert `sane_path` to a local filesystem path relative to `root`.
  161. `root` must be a secure filesystem path, it will be prepend to the path.
  162. `sane_path` must be a sanitized path without leading or trailing ``/``.
  163. Conversion of `sane_path` is done in a secure manner,
  164. or raises ``ValueError``.
  165. """
  166. assert sane_path == strip_path(sanitize_path(sane_path))
  167. safe_path = root
  168. parts = sane_path.split("/") if sane_path else []
  169. for part in parts:
  170. if not is_safe_filesystem_path_component(part):
  171. raise UnsafePathError(part)
  172. safe_path_parent = safe_path
  173. safe_path = os.path.join(safe_path, part)
  174. # Check for conflicting files (e.g. case-insensitive file systems
  175. # or short names on Windows file systems)
  176. if (os.path.lexists(safe_path) and
  177. part not in (e.name for e in
  178. os.scandir(safe_path_parent))):
  179. raise CollidingPathError(part)
  180. return safe_path
  181. class UnsafePathError(ValueError):
  182. def __init__(self, path):
  183. message = "Can't translate name safely to filesystem: %r" % path
  184. super().__init__(message)
  185. class CollidingPathError(ValueError):
  186. def __init__(self, path):
  187. message = "File name collision: %r" % path
  188. super().__init__(message)
  189. def name_from_path(path, collection):
  190. """Return Radicale item name from ``path``."""
  191. assert sanitize_path(path) == path
  192. start = unstrip_path(collection.path, True)
  193. if not (path + "/").startswith(start):
  194. raise ValueError("%r doesn't start with %r" % (path, start))
  195. name = path[len(start):]
  196. if name and not is_safe_path_component(name):
  197. raise ValueError("%r is not a component in collection %r" %
  198. (name, collection.path))
  199. return name