Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/platformdirs/unix.py: 46%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

258 statements  

1"""Unix.""" 

2 

3from __future__ import annotations 

4 

5import os 

6import posixpath 

7import re 

8import stat 

9import sys 

10import warnings 

11from contextlib import suppress 

12from pathlib import Path 

13from tempfile import gettempdir 

14from typing import TYPE_CHECKING, Final, NoReturn 

15 

16from ._xdg import XDGMixin, _expand_user, _xdg_dir 

17from .api import PlatformDirsABC, RuntimeDirWarning 

18 

19if TYPE_CHECKING: 

20 from collections.abc import Iterator 

21 

22if sys.platform == "win32": 

23 

24 def geteuid() -> NoReturn: 

25 msg = "should only be used on Unix" 

26 raise RuntimeError(msg) 

27 

28else: 

29 try: 

30 from os import geteuid 

31 except ImportError: 

32 from os import getuid as geteuid 

33 

34 

35class _UnixDefaults(PlatformDirsABC): # ruff:ignore[too-many-public-methods] 

36 """Default directories for Unix/Linux without XDG environment variable overrides. 

37 

38 The XDG env var handling is in :class:`~platformdirs._xdg.XDGMixin`. 

39 

40 """ 

41 

42 @property 

43 def _use_site(self) -> bool: 

44 return self.use_site_for_root and geteuid() == 0 

45 

46 @property 

47 def user_data_dir(self) -> str: 

48 """Data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or ``$XDG_DATA_HOME/$appname/$version``.""" 

49 return self._append_app_name_and_version(_expand_user("~/.local/share"), private=True) 

50 

51 @property 

52 def _site_data_dirs(self) -> list[str]: 

53 return [self._join_app_name_and_version("/usr/local/share"), self._join_app_name_and_version("/usr/share")] 

54 

55 @property 

56 def user_config_dir(self) -> str: 

57 """Config directory tied to the user, e.g. ``~/.config/$appname/$version`` or ``$XDG_CONFIG_HOME/$appname/$version``.""" 

58 return self._append_app_name_and_version(_expand_user("~/.config"), private=True) 

59 

60 @property 

61 def _site_config_dirs(self) -> list[str]: 

62 return [self._join_app_name_and_version("/etc/xdg")] 

63 

64 @property 

65 def user_cache_dir(self) -> str: 

66 """Cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or ``$XDG_CACHE_HOME/$appname/$version``.""" 

67 return self._append_app_name_and_version(_expand_user("~/.cache"), private=True) 

68 

69 @property 

70 def site_cache_dir(self) -> str: 

71 """Cache directory shared by users, e.g. ``/var/cache/$appname/$version``.""" 

72 return self._append_app_name_and_version("/var/cache", private=False) 

73 

74 @property 

75 def user_state_dir(self) -> str: 

76 """State directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or ``$XDG_STATE_HOME/$appname/$version``.""" 

77 return self._append_app_name_and_version(_expand_user("~/.local/state"), private=True) 

78 

79 @property 

80 def site_state_dir(self) -> str: 

81 """State directory shared by users, e.g. ``/var/lib/$appname/$version``.""" 

82 return self._append_app_name_and_version("/var/lib", private=False) 

83 

84 @property 

85 def user_log_dir(self) -> str: 

86 """Log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it.""" 

87 path = self.user_state_dir 

88 if self.opinion: 

89 path = os.path.join(path, "log") # ruff:ignore[os-path-join] 

90 self._optionally_create_directory(path, private=True) 

91 return path 

92 

93 @property 

94 def site_log_dir(self) -> str: 

95 """Log directory shared by users, e.g. ``/var/log/$appname/$version``. 

96 

97 Unlike `user_log_dir`, ``opinion`` has no effect since ``/var/log`` is inherently a log directory. 

98 

99 """ 

100 return self._append_app_name_and_version("/var/log", private=False) 

101 

102 @property 

103 def user_documents_dir(self) -> str: 

104 """Documents directory tied to the user, e.g. ``~/Documents``.""" 

105 return self._user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents") 

106 

107 @property 

108 def user_downloads_dir(self) -> str: 

109 """Downloads directory tied to the user, e.g. ``~/Downloads``.""" 

110 return self._user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads") 

111 

112 @property 

113 def user_pictures_dir(self) -> str: 

114 """Pictures directory tied to the user, e.g. ``~/Pictures``.""" 

115 return self._user_media_dir("XDG_PICTURES_DIR", "~/Pictures") 

116 

117 @property 

118 def user_videos_dir(self) -> str: 

119 """Videos directory tied to the user, e.g. ``~/Videos``.""" 

120 return self._user_media_dir("XDG_VIDEOS_DIR", "~/Videos") 

121 

122 @property 

123 def user_music_dir(self) -> str: 

124 """Music directory tied to the user, e.g. ``~/Music``.""" 

125 return self._user_media_dir("XDG_MUSIC_DIR", "~/Music") 

126 

127 @property 

128 def user_desktop_dir(self) -> str: 

129 """Desktop directory tied to the user, e.g. ``~/Desktop``.""" 

130 return self._user_media_dir("XDG_DESKTOP_DIR", "~/Desktop") 

131 

132 @property 

133 def user_projects_dir(self) -> str: 

134 """Projects directory tied to the user, e.g. ``~/Projects``.""" 

135 return self._user_media_dir("XDG_PROJECTS_DIR", "~/Projects") 

136 

137 @property 

138 def user_publicshare_dir(self) -> str: 

139 """Public share directory tied to the user, e.g. ``~/Public``.""" 

140 return self._user_media_dir("XDG_PUBLICSHARE_DIR", "~/Public") 

141 

142 @property 

143 def user_templates_dir(self) -> str: 

144 """Templates directory tied to the user, e.g. ``~/Templates``.""" 

145 return self._user_media_dir("XDG_TEMPLATES_DIR", "~/Templates") 

146 

147 def _user_media_dir(self, key: str, default: str) -> str: 

148 if path := _get_user_dirs_folder(key): 

149 self._optionally_create_media_directory(path) 

150 return path 

151 return _expand_user(default) 

152 

153 @property 

154 def user_fonts_dir(self) -> str: 

155 """Fonts directory tied to the user, e.g. ``~/.local/share/fonts``.""" 

156 return f"{_expand_user('~/.local/share')}/fonts" 

157 

158 @property 

159 def user_preference_dir(self) -> str: 

160 """Preference directory tied to the user, same as ``user_config_dir``.""" 

161 return self.user_config_dir 

162 

163 @property 

164 def user_bin_dir(self) -> str: 

165 """Bin directory tied to the user, e.g. ``~/.local/bin``.""" 

166 return _expand_user("~/.local/bin") 

167 

168 @property 

169 def site_bin_dir(self) -> str: 

170 """Bin directory shared by users, e.g. ``/usr/local/bin``.""" 

171 return "/usr/local/bin" 

172 

173 @property 

174 def user_applications_dir(self) -> str: 

175 """Applications directory tied to the user, e.g. ``~/.local/share/applications``.""" 

176 return f"{_expand_user('~/.local/share')}{os.sep}applications" 

177 

178 @property 

179 def _site_applications_dirs(self) -> list[str]: 

180 return [os.path.join(p, "applications") for p in ["/usr/local/share", "/usr/share"]] # ruff:ignore[os-path-join] 

181 

182 def _default_runtime_dir(self) -> str: 

183 if sys.platform.startswith("openbsd"): 

184 path = f"/tmp/run/user/{geteuid()}" # ruff:ignore[hardcoded-temp-file] 

185 elif sys.platform.startswith(("freebsd", "netbsd")): 

186 path = f"/var/run/user/{geteuid()}" 

187 else: 

188 path = f"/run/user/{geteuid()}" 

189 return self._temp_runtime_dir() if _runtime_dir_problem(path, missing_ok=False) else path 

190 

191 def _temp_runtime_dir(self) -> str: 

192 # Another user can pre-create this predictable name, and XDG requires an owned runtime dir with mode 0700. 

193 path = _temp_runtime_fallback() 

194 with suppress(FileExistsError): 

195 self._optionally_create_directory(path, private=True) 

196 if problem := _runtime_dir_problem(path, missing_ok=not self.ensure_exists, check_mode=False): 

197 msg = f"runtime directory {problem}; set XDG_RUNTIME_DIR instead" 

198 raise PermissionError(msg) 

199 # Earlier releases created this directory with the default 0755. 

200 if self.ensure_exists: 

201 Path(path).chmod(_RUNTIME_DIR_MODE) 

202 return path 

203 

204 @property 

205 def site_runtime_dir(self) -> str: 

206 """Runtime directory shared by users, e.g. ``/run/$appname/$version`` or ``$XDG_RUNTIME_DIR/$appname/$version``. 

207 

208 Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will fall back 

209 to paths associated to the root user instead of a regular logged-in user if it's not set. 

210 

211 If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir` 

212 instead. 

213 

214 For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set. 

215 

216 """ 

217 if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): 

218 path = "/var/run" 

219 else: 

220 path = "/run" 

221 return self._append_app_name_and_version(path, private=False) 

222 

223 

224class Unix(XDGMixin, _UnixDefaults): 

225 """On Unix/Linux, we follow the `XDG Basedir Spec <https://specifications.freedesktop.org/basedir/latest/>`_. 

226 

227 The spec allows overriding directories with environment variables. The examples shown are the default values, 

228 alongside the name of the environment variable that overrides them. Makes use of the `appname 

229 <platformdirs.api.PlatformDirsABC.appname>`, `version <platformdirs.api.PlatformDirsABC.version>`, `multipath 

230 <platformdirs.api.PlatformDirsABC.multipath>`, `opinion <platformdirs.api.PlatformDirsABC.opinion>`, `ensure_exists 

231 <platformdirs.api.PlatformDirsABC.ensure_exists>`. 

232 

233 """ 

234 

235 @property 

236 def user_data_dir(self) -> str: 

237 """Data directory tied to the user, or site equivalent when root with ``use_site_for_root``.""" 

238 return self.site_data_dir if self._use_site else super().user_data_dir 

239 

240 @property 

241 def user_config_dir(self) -> str: 

242 """Config directory tied to the user, or site equivalent when root with ``use_site_for_root``.""" 

243 return self.site_config_dir if self._use_site else super().user_config_dir 

244 

245 @property 

246 def user_cache_dir(self) -> str: 

247 """Cache directory tied to the user, or site equivalent when root with ``use_site_for_root``.""" 

248 return self.site_cache_dir if self._use_site else super().user_cache_dir 

249 

250 @property 

251 def user_state_dir(self) -> str: 

252 """State directory tied to the user, or site equivalent when root with ``use_site_for_root``.""" 

253 return self.site_state_dir if self._use_site else super().user_state_dir 

254 

255 @property 

256 def user_log_dir(self) -> str: 

257 """Log directory tied to the user, or site equivalent when root with ``use_site_for_root``.""" 

258 return self.site_log_dir if self._use_site else super().user_log_dir 

259 

260 @property 

261 def user_applications_dir(self) -> str: 

262 """Applications directory tied to the user, or site equivalent when root with ``use_site_for_root``.""" 

263 return self.site_applications_dir if self._use_site else super().user_applications_dir 

264 

265 @property 

266 def user_runtime_dir(self) -> str: 

267 """Runtime directory tied to the user, e.g. ``$XDG_RUNTIME_DIR/$appname/$version``. 

268 

269 Accepts ``$XDG_RUNTIME_DIR`` only as a directory, not a symlink, that the user owns with mode ``0700``, as the 

270 XDG spec requires, and only below parents no other user can replace. Creates a missing one that way under 

271 ``ensure_exists``. When the variable is unset or fails that check, emits one 

272 :class:`~platformdirs.RuntimeDirWarning` per cause and falls back to the platform default 

273 (``/tmp/run/user/<uid>`` on OpenBSD, ``/var/run/user/<uid>`` on FreeBSD/NetBSD, ``/run/user/<uid>`` elsewhere) 

274 if it passes the same check, else to ``runtime-<uid>`` in the temporary directory. Root with 

275 ``use_site_for_root`` gets the site equivalent. 

276 

277 :raises PermissionError: if the temporary fallback is a symlink, not a directory, owned by another user, or 

278 missing right after its creation. 

279 

280 """ 

281 if self._use_site: 

282 # XDGMixin.site_runtime_dir reads $XDG_RUNTIME_DIR, which belongs to the user who started the root process. 

283 return super(XDGMixin, self).site_runtime_dir 

284 if not (path := _xdg_dir("XDG_RUNTIME_DIR")): 

285 reason = "XDG_RUNTIME_DIR is not set" 

286 else: 

287 # Creating the missing levels follows whatever symlink sits at a parent, so check the parents first. 

288 if not (skipped := bool(_runtime_dir_parent_problem(path))): 

289 with suppress(OSError): 

290 self._optionally_create_directory(path, private=True) 

291 # A leaf left missing because the parent check skipped creation is reported as that parent problem. 

292 if not ( 

293 problem := _runtime_dir_problem(path, missing_ok=skipped or not self.ensure_exists) 

294 or _runtime_dir_parent_problem(path) 

295 ): 

296 return self._append_app_name_and_version(path, private=True) 

297 reason = f"XDG_RUNTIME_DIR {problem}" 

298 _warn_once(f"{reason}, falling back to {(fallback := self._default_runtime_dir())}") 

299 return self._append_app_name_and_version(fallback, private=True) 

300 

301 def place_runtime_file(self, name: str | os.PathLike[str]) -> Path: 

302 """Like :meth:`~platformdirs.api.PlatformDirsABC.place_runtime_file`. 

303 

304 Creates the runtime directory before checking it, so another user cannot create it in between. 

305 

306 :raises PermissionError: if the temporary fallback is a symlink, not a directory, owned by another user, or 

307 missing right after its creation. 

308 

309 """ 

310 

311 def created(lookup: PlatformDirsABC) -> Path: 

312 lookup.ensure_exists = True 

313 return lookup.user_runtime_path 

314 

315 return self._place_file(created, name) 

316 

317 def _find_runtime_files(self, name: str | os.PathLike[str]) -> Iterator[Path]: 

318 return filter(_in_own_runtime_dir, super()._find_runtime_files(name)) 

319 

320 @property 

321 def user_bin_dir(self) -> str: 

322 """Bin directory tied to the user, or site equivalent when root with ``use_site_for_root``.""" 

323 return self.site_bin_dir if self._use_site else super().user_bin_dir 

324 

325 @property 

326 def user_fonts_dir(self) -> str: 

327 """Fonts directory tied to the user, ignoring ``$XDG_DATA_HOME`` when root with ``use_site_for_root``.""" 

328 return super(XDGMixin, self).user_fonts_dir if self._use_site else super().user_fonts_dir 

329 

330 def _xdg_media_dir(self, env_var: str) -> str | None: 

331 # The XDG_*_DIR variables and user-dirs.dirs belong to the user who started the root process. 

332 return None if self._use_site else super()._xdg_media_dir(env_var) 

333 

334 def _user_media_dir(self, key: str, default: str) -> str: 

335 return _expand_user(default) if self._use_site else super()._user_media_dir(key, default) 

336 

337 @property 

338 def user_data_path(self) -> Path: 

339 """Data path tied to the user, or the first site entry when root with ``use_site_for_root``.""" 

340 return self.site_data_path if self._use_site else super().user_data_path 

341 

342 @property 

343 def user_config_path(self) -> Path: 

344 """Config path tied to the user, or the first site entry when root with ``use_site_for_root``.""" 

345 return self.site_config_path if self._use_site else super().user_config_path 

346 

347 @property 

348 def user_preference_path(self) -> Path: 

349 """Preference path tied to the user, or the first site config entry when root with ``use_site_for_root``.""" 

350 return self.site_config_path if self._use_site else super().user_preference_path 

351 

352 @property 

353 def user_applications_path(self) -> Path: 

354 """Applications path tied to the user, or the first site entry when root with ``use_site_for_root``.""" 

355 return self.site_applications_path if self._use_site else super().user_applications_path 

356 

357 

358_USER_DIRS_LINE: Final = re.compile( 

359 r""" 

360 [ \t]*(?P<key>\w+)[ \t]*=[ \t]* # KEY=, blanks allowed around the key and after the = 

361 (?: 

362 "(?P<quoted>(?:[^"\\]|\\.)*)" # a double-quoted value with backslash escapes, text after it ignored 

363 |(?P<bare>[^"\s](?:[^ \t\n]|[ \t]+[^ \t\n\#])*) # or an unquoted value; blanks join it only before a word 

364 # that starts no comment, and each run is matched once so 

365 # long runs stay linear 

366 ) 

367 """, 

368 re.VERBOSE, 

369) 

370 

371 

372def _get_user_dirs_folder(key: str) -> str | None: 

373 """Return directory from user-dirs.dirs config file. 

374 

375 ``xdg-user-dirs-update`` writes shell assignments, so match each line like ``xdg-user-dir`` does and keep the last 

376 valid assignment to ``key``. 

377 

378 See https://freedesktop.org/wiki/Software/xdg-user-dirs/. 

379 

380 """ 

381 config_home = _xdg_dir("XDG_CONFIG_HOME") or _expand_user("~/.config") 

382 user_dirs_config_path = Path(config_home) / "user-dirs.dirs" 

383 folder = None 

384 try: 

385 # the file holds raw path bytes, and surrogateescape round-trips undecodable ones like os.fsdecode 

386 with user_dirs_config_path.open(encoding=sys.getfilesystemencoding(), errors="surrogateescape") as stream: 

387 for line in stream: 

388 if (entry := _USER_DIRS_LINE.match(line)) and entry["key"] == key: 

389 folder = _resolve_user_dirs_value(entry) or folder 

390 except OSError: # a missing or unreadable file leaves the defaults in place 

391 return None 

392 return folder 

393 

394 

395def _resolve_user_dirs_value(entry: re.Match[str]) -> str | None: 

396 value = entry["bare"] if entry["quoted"] is None else entry["quoted"] 

397 if value == "$HOME" or value.startswith("$HOME/"): 

398 # A home of / would give "//Desktop", which pathlib keeps as a distinct root. 

399 prefix, value = _expand_user("~").rstrip("/"), value.removeprefix("$HOME") 

400 elif value.startswith("/"): 

401 prefix = "" 

402 else: 

403 return None 

404 if entry["quoted"] is not None: 

405 # xdg-user-dirs-update backslash-escapes $, `, " and \ inside the quotes. 

406 value = re.sub(r'\\([$`"\\])', r"\1", value) 

407 return f"{prefix}{value}" or "/" 

408 

409 

410def _temp_runtime_fallback() -> str: 

411 # A temporary directory of / would give "//runtime-<uid>", which pathlib keeps as a distinct root. 

412 return posixpath.join(gettempdir(), f"runtime-{geteuid()}") 

413 

414 

415def _in_own_runtime_dir(path: Path) -> bool: 

416 # The lookup let a missing runtime directory pass, so another user may have created it since. The fallback goes 

417 # first because a rejected XDG_RUNTIME_DIR, such as /tmp, contains it. 

418 for base in (_temp_runtime_fallback(), _xdg_dir("XDG_RUNTIME_DIR")): 

419 if base and path.is_relative_to(base): 

420 return not _runtime_dir_problem(base, missing_ok=False, check_mode=False) 

421 return True 

422 

423 

424def _runtime_dir_problem(path: str, *, missing_ok: bool, check_mode: bool = True) -> str | None: 

425 """Return why ``path`` fails the checks of Qt's ``checkXdgRuntimeDir``, or ``None`` if it passes. 

426 

427 A missing ``path`` passes under ``missing_ok``. A caller that then creates it or reads below it has to check again, 

428 since another user can create it in between. 

429 

430 """ 

431 try: 

432 # A trailing / or /. makes lstat follow a final symlink, while Path, which creates the directory, drops it. 

433 info = os.lstat(str(Path(path))) 

434 except FileNotFoundError: 

435 return None if missing_ok else f"{path} is missing" 

436 except OSError as error: 

437 return f"{path} cannot be checked: {error.strerror}" 

438 mode = info.st_mode & 0o777 

439 checks = ( 

440 (stat.S_ISLNK(info.st_mode), "is a symlink"), 

441 (not stat.S_ISDIR(info.st_mode), "is not a directory"), 

442 (info.st_uid != (uid := geteuid()), f"is owned by uid {info.st_uid}, not {uid}"), 

443 (check_mode and mode != _RUNTIME_DIR_MODE, f"has mode {mode:04o}, not {_RUNTIME_DIR_MODE:04o}"), 

444 ) 

445 return next((f"{path} {problem}" for failed, problem in checks if failed), None) 

446 

447 

448def _runtime_dir_parent_problem(path: str) -> str | None: 

449 if sys.platform == "win32": # every directory reports as writable by everyone there 

450 return None 

451 in_shared_dir = False 

452 for parent in reversed(Path(path).parents): 

453 try: 

454 if in_shared_dir and (owner := os.lstat(parent).st_uid) not in {0, geteuid()}: 

455 return f"{path} is below {parent}, owned by uid {owner}" 

456 mode = parent.stat().st_mode # a symlink that got past the owner check is judged by its target 

457 except OSError: 

458 return None # the missing rest gets created, or _runtime_dir_problem reports the path itself 

459 if (in_shared_dir := bool(mode & stat.S_IWOTH)) and not mode & stat.S_ISVTX: 

460 return f"{path} is below {parent}, writable by every user" 

461 return None 

462 

463 

464def _warn_once(message: str) -> None: 

465 # Each read of user_runtime_dir repeats the check, so a warning per read would flood long-running programs. 

466 if message not in _WARNED: 

467 _WARNED.add(message) 

468 warnings.warn(message, RuntimeDirWarning, stacklevel=3) 

469 

470 

471_WARNED: Final[set[str]] = set() 

472_RUNTIME_DIR_MODE: Final = 0o700 

473 

474 

475__all__ = [ 

476 "Unix", 

477]