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