1// Copyright (C) 2020 The Qt Company Ltd.
2// Copyright (C) 2020 Intel Corporation.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "qstandardpaths.h"
6#include <qdir.h>
7#include <qfile.h>
8#include <qhash.h>
9#include <qtextstream.h>
10#if QT_CONFIG(regularexpression)
11#include <qregularexpression.h>
12#endif
13#include <private/qfilesystemengine_p.h>
14#include <errno.h>
15#include <stdlib.h>
16
17#ifndef QT_BOOTSTRAPPED
18#include <qcoreapplication.h>
19#endif
20
21#ifndef QT_NO_STANDARDPATHS
22
23QT_BEGIN_NAMESPACE
24
25using namespace Qt::StringLiterals;
26
27static void appendOrganizationAndApp(QString &path)
28{
29#ifndef QT_BOOTSTRAPPED
30 const QString org = QCoreApplication::organizationName();
31 if (!org.isEmpty())
32 path += u'/' + org;
33 const QString appName = QCoreApplication::applicationName();
34 if (!appName.isEmpty())
35 path += u'/' + appName;
36#else
37 Q_UNUSED(path);
38#endif
39}
40
41#if QT_CONFIG(regularexpression)
42static QLatin1StringView xdg_key_name(QStandardPaths::StandardLocation type)
43{
44 switch (type) {
45 case QStandardPaths::DesktopLocation:
46 return "DESKTOP"_L1;
47 case QStandardPaths::DocumentsLocation:
48 return "DOCUMENTS"_L1;
49 case QStandardPaths::PicturesLocation:
50 return "PICTURES"_L1;
51 case QStandardPaths::MusicLocation:
52 return "MUSIC"_L1;
53 case QStandardPaths::MoviesLocation:
54 return "VIDEOS"_L1;
55 case QStandardPaths::DownloadLocation:
56 return "DOWNLOAD"_L1;
57 case QStandardPaths::PublicShareLocation:
58 return "PUBLICSHARE"_L1;
59 case QStandardPaths::TemplatesLocation:
60 return "TEMPLATES"_L1;
61 default:
62 return {};
63 }
64}
65#endif
66
67static QByteArray unixPermissionsText(QFile::Permissions permissions)
68{
69 mode_t perms = 0;
70 if (permissions & QFile::ReadOwner)
71 perms |= S_IRUSR;
72 if (permissions & QFile::WriteOwner)
73 perms |= S_IWUSR;
74 if (permissions & QFile::ExeOwner)
75 perms |= S_IXUSR;
76 if (permissions & QFile::ReadGroup)
77 perms |= S_IRGRP;
78 if (permissions & QFile::WriteGroup)
79 perms |= S_IWGRP;
80 if (permissions & QFile::ExeGroup)
81 perms |= S_IXGRP;
82 if (permissions & QFile::ReadOther)
83 perms |= S_IROTH;
84 if (permissions & QFile::WriteOther)
85 perms |= S_IWOTH;
86 if (permissions & QFile::ExeOther)
87 perms |= S_IXOTH;
88 return '0' + QByteArray::number(perms, base: 8);
89}
90
91static bool checkXdgRuntimeDir(const QString &xdgRuntimeDir)
92{
93 auto describeMetaData = [](const QFileSystemMetaData &metaData) -> QByteArray {
94 if (!metaData.exists())
95 return "a broken symlink";
96
97 QByteArray description;
98 if (metaData.isLink())
99 description = "a symbolic link to ";
100
101 if (metaData.isFile())
102 description += "a regular file";
103 else if (metaData.isDirectory())
104 description += "a directory";
105 else if (metaData.isSequential())
106 description += "a character device, socket or FIFO";
107 else
108 description += "a block device";
109
110 description += " permissions " + unixPermissionsText(permissions: metaData.permissions());
111
112 return description
113 + " owned by UID " + QByteArray::number(metaData.userId())
114 + " GID " + QByteArray::number(metaData.groupId());
115 };
116
117 // http://standards.freedesktop.org/basedir-spec/latest/
118 const uint myUid = uint(geteuid());
119 const QFile::Permissions wantedPerms = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
120 const QFileSystemMetaData::MetaDataFlags statFlags = QFileSystemMetaData::PosixStatFlags
121 | QFileSystemMetaData::LinkType;
122 QFileSystemMetaData metaData;
123 QFileSystemEntry entry(xdgRuntimeDir);
124
125 // Check that the xdgRuntimeDir is a directory by attempting to create it.
126 // A stat() before mkdir() that concluded it doesn't exist is a meaningless
127 // result: we'd race against someone else attempting to create it.
128 // ### QFileSystemEngine::createDirectory cannot take the extra mode argument.
129 if (QT_MKDIR(path: entry.nativeFilePath(), mode: 0700) == 0)
130 return true;
131 if (errno != EEXIST) {
132 qErrnoWarning(msg: "QStandardPaths: error creating runtime directory '%ls'",
133 qUtf16Printable(xdgRuntimeDir));
134 return false;
135 }
136
137 // We use LinkType to force an lstat(), but fillMetaData() still returns error
138 // on broken symlinks.
139 if (!QFileSystemEngine::fillMetaData(entry, data&: metaData, what: statFlags) && !metaData.isLink()) {
140 qErrnoWarning(msg: "QStandardPaths: error obtaining permissions of runtime directory '%ls'",
141 qUtf16Printable(xdgRuntimeDir));
142 return false;
143 }
144
145 // Checks:
146 // - is a directory
147 // - is not a symlink (even is pointing to a directory)
148 if (metaData.isLink() || !metaData.isDirectory()) {
149 qWarning(msg: "QStandardPaths: runtime directory '%ls' is not a directory, but %s",
150 qUtf16Printable(xdgRuntimeDir), describeMetaData(metaData).constData());
151 return false;
152 }
153
154 // - "The directory MUST be owned by the user"
155 if (metaData.userId() != myUid) {
156 qWarning(msg: "QStandardPaths: runtime directory '%ls' is not owned by UID %d, but %s",
157 qUtf16Printable(xdgRuntimeDir), myUid, describeMetaData(metaData).constData());
158 return false;
159 }
160
161 // "and he MUST be the only one having read and write access to it. Its Unix access mode MUST be 0700."
162 if (metaData.permissions() != wantedPerms) {
163 qWarning(msg: "QStandardPaths: wrong permissions on runtime directory %ls, %s instead of %s",
164 qUtf16Printable(xdgRuntimeDir),
165 unixPermissionsText(permissions: metaData.permissions()).constData(),
166 unixPermissionsText(permissions: wantedPerms).constData());
167 return false;
168 }
169
170 return true;
171}
172
173QString QStandardPaths::writableLocation(StandardLocation type)
174{
175 switch (type) {
176 case HomeLocation:
177 return QDir::homePath();
178 case TempLocation:
179 return QDir::tempPath();
180 case CacheLocation:
181 case GenericCacheLocation:
182 {
183 QString xdgCacheHome;
184 if (isTestModeEnabled()) {
185 xdgCacheHome = QDir::homePath() + "/.qttest/cache"_L1;
186 } else {
187 // http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
188 xdgCacheHome = QFile::decodeName(localFileName: qgetenv(varName: "XDG_CACHE_HOME"));
189 if (!xdgCacheHome.startsWith(c: u'/'))
190 xdgCacheHome.clear(); // spec says relative paths should be ignored
191
192 if (xdgCacheHome.isEmpty())
193 xdgCacheHome = QDir::homePath() + "/.cache"_L1;
194 }
195 if (type == QStandardPaths::CacheLocation)
196 appendOrganizationAndApp(path&: xdgCacheHome);
197 return xdgCacheHome;
198 }
199 case StateLocation:
200 case GenericStateLocation:
201 {
202 QString xdgStateHome;
203 if (isTestModeEnabled()) {
204 xdgStateHome = QDir::homePath() + "/.qttest/state"_L1;
205 } else {
206 // http://standards.freedesktop.org/basedir-spec/basedir-spec-0.8.html
207 xdgStateHome = QFile::decodeName(localFileName: qgetenv(varName: "XDG_STATE_HOME"));
208 if (!xdgStateHome.startsWith(c: u'/'))
209 xdgStateHome.clear(); // spec says relative paths should be ignored
210
211 if (xdgStateHome.isEmpty())
212 xdgStateHome = QDir::homePath() + "/.local/state"_L1;
213 }
214 if (type == QStandardPaths::StateLocation)
215 appendOrganizationAndApp(path&: xdgStateHome);
216 return xdgStateHome;
217 }
218 case AppDataLocation:
219 case AppLocalDataLocation:
220 case GenericDataLocation:
221 {
222 QString xdgDataHome;
223 if (isTestModeEnabled()) {
224 xdgDataHome = QDir::homePath() + "/.qttest/share"_L1;
225 } else {
226 xdgDataHome = QFile::decodeName(localFileName: qgetenv(varName: "XDG_DATA_HOME"));
227 if (!xdgDataHome.startsWith(c: u'/'))
228 xdgDataHome.clear(); // spec says relative paths should be ignored
229
230 if (xdgDataHome.isEmpty())
231 xdgDataHome = QDir::homePath() + "/.local/share"_L1;
232 }
233 if (type == AppDataLocation || type == AppLocalDataLocation)
234 appendOrganizationAndApp(path&: xdgDataHome);
235 return xdgDataHome;
236 }
237 case ConfigLocation:
238 case GenericConfigLocation:
239 case AppConfigLocation:
240 {
241 QString xdgConfigHome;
242 if (isTestModeEnabled()) {
243 xdgConfigHome = QDir::homePath() + "/.qttest/config"_L1;
244 } else {
245 // http://standards.freedesktop.org/basedir-spec/latest/
246 xdgConfigHome = QFile::decodeName(localFileName: qgetenv(varName: "XDG_CONFIG_HOME"));
247 if (!xdgConfigHome.startsWith(c: u'/'))
248 xdgConfigHome.clear(); // spec says relative paths should be ignored
249
250 if (xdgConfigHome.isEmpty())
251 xdgConfigHome = QDir::homePath() + "/.config"_L1;
252 }
253 if (type == AppConfigLocation)
254 appendOrganizationAndApp(path&: xdgConfigHome);
255 return xdgConfigHome;
256 }
257 case RuntimeLocation:
258 {
259 QString xdgRuntimeDir = QFile::decodeName(localFileName: qgetenv(varName: "XDG_RUNTIME_DIR"));
260 if (!xdgRuntimeDir.startsWith(c: u'/'))
261 xdgRuntimeDir.clear(); // spec says relative paths should be ignored
262
263 bool fromEnv = !xdgRuntimeDir.isEmpty();
264 if (xdgRuntimeDir.isEmpty() || !checkXdgRuntimeDir(xdgRuntimeDir)) {
265 // environment variable not set or is set to something unsuitable
266 const uint myUid = uint(geteuid());
267 const QString userName = QFileSystemEngine::resolveUserName(userId: myUid);
268 xdgRuntimeDir = QDir::tempPath() + "/runtime-"_L1 + userName;
269
270 if (!fromEnv) {
271#ifndef Q_OS_WASM
272 qWarning(msg: "QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '%ls'", qUtf16Printable(xdgRuntimeDir));
273#endif
274 }
275
276 if (!checkXdgRuntimeDir(xdgRuntimeDir))
277 xdgRuntimeDir.clear();
278 }
279
280 return xdgRuntimeDir;
281 }
282 default:
283 break;
284 }
285
286#if QT_CONFIG(regularexpression)
287 // http://www.freedesktop.org/wiki/Software/xdg-user-dirs
288 QString xdgConfigHome = QFile::decodeName(localFileName: qgetenv(varName: "XDG_CONFIG_HOME"));
289 if (!xdgConfigHome.startsWith(c: u'/'))
290 xdgConfigHome.clear(); // spec says relative paths should be ignored
291
292 if (xdgConfigHome.isEmpty())
293 xdgConfigHome = QDir::homePath() + "/.config"_L1;
294 QFile file(xdgConfigHome + "/user-dirs.dirs"_L1);
295 const QLatin1StringView key = xdg_key_name(type);
296 if (!key.isEmpty() && !isTestModeEnabled() && file.open(flags: QIODevice::ReadOnly)) {
297 QTextStream stream(&file);
298 // Only look for lines like: XDG_DESKTOP_DIR="$HOME/Desktop"
299 static const QRegularExpression exp(u"^XDG_(.*)_DIR=(.*)$"_s);
300 QString result;
301 while (!stream.atEnd()) {
302 const QString &line = stream.readLine();
303 QRegularExpressionMatch match = exp.match(subject: line);
304 if (match.hasMatch() && match.capturedView(nth: 1) == key) {
305 QStringView value = match.capturedView(nth: 2);
306 if (value.size() > 2
307 && value.startsWith(c: u'\"')
308 && value.endsWith(c: u'\"'))
309 value = value.mid(pos: 1, n: value.size() - 2);
310 // value can start with $HOME
311 if (value.startsWith(s: "$HOME"_L1))
312 result = QDir::homePath() + value.mid(pos: 5);
313 else
314 result = value.toString();
315 if (result.size() > 1 && result.endsWith(c: u'/'))
316 result.chop(n: 1);
317 }
318 }
319 if (!result.isNull())
320 return result;
321 }
322#endif // QT_CONFIG(regularexpression)
323
324 QString path;
325 switch (type) {
326 case DesktopLocation:
327 path = QDir::homePath() + "/Desktop"_L1;
328 break;
329 case DocumentsLocation:
330 path = QDir::homePath() + "/Documents"_L1;
331 break;
332 case PicturesLocation:
333 path = QDir::homePath() + "/Pictures"_L1;
334 break;
335
336 case FontsLocation:
337 path = writableLocation(type: GenericDataLocation) + "/fonts"_L1;
338 break;
339
340 case MusicLocation:
341 path = QDir::homePath() + "/Music"_L1;
342 break;
343
344 case MoviesLocation:
345 path = QDir::homePath() + "/Videos"_L1;
346 break;
347 case DownloadLocation:
348 path = QDir::homePath() + "/Downloads"_L1;
349 break;
350 case ApplicationsLocation:
351 path = writableLocation(type: GenericDataLocation) + "/applications"_L1;
352 break;
353
354 case PublicShareLocation:
355 path = QDir::homePath() + "/Public"_L1;
356 break;
357
358 case TemplatesLocation:
359 path = QDir::homePath() + "/Templates"_L1;
360 break;
361
362 default:
363 break;
364 }
365
366 return path;
367}
368
369static QStringList dirsList(const QString &xdgEnvVar)
370{
371 QStringList dirs;
372 // http://standards.freedesktop.org/basedir-spec/latest/
373 // Normalize paths, skip relative paths (the spec says relative paths
374 // should be ignored)
375 for (const auto dir : qTokenize(h: xdgEnvVar, n: u':'))
376 if (dir.startsWith(c: u'/'))
377 dirs.push_back(t: QDir::cleanPath(path: dir.toString()));
378
379 // Remove duplicates from the list, there's no use for duplicated paths
380 // in XDG_* env vars - if whatever is being looked for is not found in
381 // the given directory the first time, it won't be there the second time.
382 // Plus duplicate paths causes problems for example for mimetypes,
383 // where duplicate paths here lead to duplicated mime types returned
384 // for a file, eg "text/plain,text/plain" instead of "text/plain"
385 dirs.removeDuplicates();
386
387 return dirs;
388}
389
390static QStringList xdgDataDirs()
391{
392 // http://standards.freedesktop.org/basedir-spec/latest/
393 QString xdgDataDirsEnv = QFile::decodeName(localFileName: qgetenv(varName: "XDG_DATA_DIRS"));
394
395 QStringList dirs = dirsList(xdgEnvVar: xdgDataDirsEnv);
396 if (dirs.isEmpty())
397 dirs = QStringList{u"/usr/local/share"_s, u"/usr/share"_s};
398
399 return dirs;
400}
401
402static QStringList xdgConfigDirs()
403{
404 // http://standards.freedesktop.org/basedir-spec/latest/
405 const QString xdgConfigDirs = QFile::decodeName(localFileName: qgetenv(varName: "XDG_CONFIG_DIRS"));
406
407 QStringList dirs = dirsList(xdgEnvVar: xdgConfigDirs);
408 if (dirs.isEmpty())
409 dirs.push_back(t: u"/etc/xdg"_s);
410
411 return dirs;
412}
413
414QStringList QStandardPaths::standardLocations(StandardLocation type)
415{
416 QStringList dirs;
417 switch (type) {
418 case ConfigLocation:
419 case GenericConfigLocation:
420 dirs = xdgConfigDirs();
421 break;
422 case AppConfigLocation:
423 dirs = xdgConfigDirs();
424 for (int i = 0; i < dirs.size(); ++i)
425 appendOrganizationAndApp(path&: dirs[i]);
426 break;
427 case GenericDataLocation:
428 dirs = xdgDataDirs();
429 break;
430 case ApplicationsLocation:
431 dirs = xdgDataDirs();
432 for (int i = 0; i < dirs.size(); ++i)
433 dirs[i].append(s: "/applications"_L1);
434 break;
435 case AppDataLocation:
436 case AppLocalDataLocation:
437 dirs = xdgDataDirs();
438 for (int i = 0; i < dirs.size(); ++i)
439 appendOrganizationAndApp(path&: dirs[i]);
440 break;
441 case FontsLocation:
442 dirs += QDir::homePath() + "/.fonts"_L1;
443 dirs += xdgDataDirs();
444 for (int i = 1; i < dirs.size(); ++i)
445 dirs[i].append(s: "/fonts"_L1);
446 break;
447 default:
448 break;
449 }
450 const QString localDir = writableLocation(type);
451 dirs.prepend(t: localDir);
452 return dirs;
453}
454
455QT_END_NAMESPACE
456
457#endif // QT_NO_STANDARDPATHS
458

source code of qtbase/src/corelib/io/qstandardpaths_unix.cpp