pathutils.py 7.6 KB

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