qtbase/src/corelib/io/qstandardpaths_unix.cpp
Ahmad Samir 37fcf98d42 QStandardPaths/Unix: fix logic in xdgDataDirs() function
This method correctly ignores relative paths (as per the XDG basedir
spec), but checking the list of dirs is empty should be moved to after
splitting the env var, because even if the env var is not empty, if the
paths in it are all relative the resulting list will be empty.

Drive-by change: Split some code to a static helper, which will be
used in xdgConfigDirs() too.

Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
Change-Id: If894751ba68b24ccc214f9a4bb2099be3f0e4349
(cherry picked from commit ee515dd842d79fa4543568ed82bd7c949923e438)
(cherry picked from commit 668560f8bd13bd1478c1837ef0dc0eb33f6445f9)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
2023-02-07 22:34:42 +00:00

418 lines
14 KiB
C++

// Copyright (C) 2020 The Qt Company Ltd.
// Copyright (C) 2020 Intel Corporation.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qstandardpaths.h"
#include <qdir.h>
#include <qfile.h>
#include <qhash.h>
#include <qtextstream.h>
#if QT_CONFIG(regularexpression)
#include <qregularexpression.h>
#endif
#include <private/qfilesystemengine_p.h>
#include <errno.h>
#include <stdlib.h>
#ifndef QT_BOOTSTRAPPED
#include <qcoreapplication.h>
#endif
#ifndef QT_NO_STANDARDPATHS
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
static void appendOrganizationAndApp(QString &path)
{
#ifndef QT_BOOTSTRAPPED
const QString org = QCoreApplication::organizationName();
if (!org.isEmpty())
path += u'/' + org;
const QString appName = QCoreApplication::applicationName();
if (!appName.isEmpty())
path += u'/' + appName;
#else
Q_UNUSED(path);
#endif
}
#if QT_CONFIG(regularexpression)
static QLatin1StringView xdg_key_name(QStandardPaths::StandardLocation type)
{
switch (type) {
case QStandardPaths::DesktopLocation:
return "DESKTOP"_L1;
case QStandardPaths::DocumentsLocation:
return "DOCUMENTS"_L1;
case QStandardPaths::PicturesLocation:
return "PICTURES"_L1;
case QStandardPaths::MusicLocation:
return "MUSIC"_L1;
case QStandardPaths::MoviesLocation:
return "VIDEOS"_L1;
case QStandardPaths::DownloadLocation:
return "DOWNLOAD"_L1;
case QStandardPaths::PublicShareLocation:
return "PUBLICSHARE"_L1;
case QStandardPaths::TemplatesLocation:
return "TEMPLATES"_L1;
default:
return {};
}
}
#endif
static QByteArray unixPermissionsText(QFile::Permissions permissions)
{
mode_t perms = 0;
if (permissions & QFile::ReadOwner)
perms |= S_IRUSR;
if (permissions & QFile::WriteOwner)
perms |= S_IWUSR;
if (permissions & QFile::ExeOwner)
perms |= S_IXUSR;
if (permissions & QFile::ReadGroup)
perms |= S_IRGRP;
if (permissions & QFile::WriteGroup)
perms |= S_IWGRP;
if (permissions & QFile::ExeGroup)
perms |= S_IXGRP;
if (permissions & QFile::ReadOther)
perms |= S_IROTH;
if (permissions & QFile::WriteOther)
perms |= S_IWOTH;
if (permissions & QFile::ExeOther)
perms |= S_IXOTH;
return '0' + QByteArray::number(perms, 8);
}
static bool checkXdgRuntimeDir(const QString &xdgRuntimeDir)
{
auto describeMetaData = [](const QFileSystemMetaData &metaData) -> QByteArray {
if (!metaData.exists())
return "a broken symlink";
QByteArray description;
if (metaData.isLink())
description = "a symbolic link to ";
if (metaData.isFile())
description += "a regular file";
else if (metaData.isDirectory())
description += "a directory";
else if (metaData.isSequential())
description += "a character device, socket or FIFO";
else
description += "a block device";
description += " permissions " + unixPermissionsText(metaData.permissions());
return description
+ " owned by UID " + QByteArray::number(metaData.userId())
+ " GID " + QByteArray::number(metaData.groupId());
};
// http://standards.freedesktop.org/basedir-spec/latest/
const uint myUid = uint(geteuid());
const QFile::Permissions wantedPerms = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
const QFileSystemMetaData::MetaDataFlags statFlags = QFileSystemMetaData::PosixStatFlags
| QFileSystemMetaData::LinkType;
QFileSystemMetaData metaData;
QFileSystemEntry entry(xdgRuntimeDir);
// Check that the xdgRuntimeDir is a directory by attempting to create it.
// A stat() before mkdir() that concluded it doesn't exist is a meaningless
// result: we'd race against someone else attempting to create it.
// ### QFileSystemEngine::createDirectory cannot take the extra mode argument.
if (QT_MKDIR(entry.nativeFilePath(), 0700) == 0)
return true;
if (errno != EEXIST) {
qErrnoWarning("QStandardPaths: error creating runtime directory '%ls'",
qUtf16Printable(xdgRuntimeDir));
return false;
}
// We use LinkType to force an lstat(), but fillMetaData() still returns error
// on broken symlinks.
if (!QFileSystemEngine::fillMetaData(entry, metaData, statFlags) && !metaData.isLink()) {
qErrnoWarning("QStandardPaths: error obtaining permissions of runtime directory '%ls'",
qUtf16Printable(xdgRuntimeDir));
return false;
}
// Checks:
// - is a directory
// - is not a symlink (even is pointing to a directory)
if (metaData.isLink() || !metaData.isDirectory()) {
qWarning("QStandardPaths: runtime directory '%ls' is not a directory, but %s",
qUtf16Printable(xdgRuntimeDir), describeMetaData(metaData).constData());
return false;
}
// - "The directory MUST be owned by the user"
if (metaData.userId() != myUid) {
qWarning("QStandardPaths: runtime directory '%ls' is not owned by UID %d, but %s",
qUtf16Printable(xdgRuntimeDir), myUid, describeMetaData(metaData).constData());
return false;
}
// "and he MUST be the only one having read and write access to it. Its Unix access mode MUST be 0700."
if (metaData.permissions() != wantedPerms) {
qWarning("QStandardPaths: wrong permissions on runtime directory %ls, %s instead of %s",
qUtf16Printable(xdgRuntimeDir),
unixPermissionsText(metaData.permissions()).constData(),
unixPermissionsText(wantedPerms).constData());
return false;
}
return true;
}
QString QStandardPaths::writableLocation(StandardLocation type)
{
switch (type) {
case HomeLocation:
return QDir::homePath();
case TempLocation:
return QDir::tempPath();
case CacheLocation:
case GenericCacheLocation:
{
if (isTestModeEnabled())
return QDir::homePath() + "/.qttest/cache"_L1;
// http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
QString xdgCacheHome = QFile::decodeName(qgetenv("XDG_CACHE_HOME"));
if (xdgCacheHome.isEmpty())
xdgCacheHome = QDir::homePath() + "/.cache"_L1;
if (type == QStandardPaths::CacheLocation)
appendOrganizationAndApp(xdgCacheHome);
return xdgCacheHome;
}
case AppDataLocation:
case AppLocalDataLocation:
case GenericDataLocation:
{
if (isTestModeEnabled())
return QDir::homePath() + "/.qttest/share"_L1;
QString xdgDataHome = QFile::decodeName(qgetenv("XDG_DATA_HOME"));
if (xdgDataHome.isEmpty())
xdgDataHome = QDir::homePath() + "/.local/share"_L1;
if (type == AppDataLocation || type == AppLocalDataLocation)
appendOrganizationAndApp(xdgDataHome);
return xdgDataHome;
}
case ConfigLocation:
case GenericConfigLocation:
case AppConfigLocation:
{
if (isTestModeEnabled())
return QDir::homePath() + "/.qttest/config"_L1;
// http://standards.freedesktop.org/basedir-spec/latest/
QString xdgConfigHome = QFile::decodeName(qgetenv("XDG_CONFIG_HOME"));
if (xdgConfigHome.isEmpty())
xdgConfigHome = QDir::homePath() + "/.config"_L1;
if (type == AppConfigLocation)
appendOrganizationAndApp(xdgConfigHome);
return xdgConfigHome;
}
case RuntimeLocation:
{
QString xdgRuntimeDir = QFile::decodeName(qgetenv("XDG_RUNTIME_DIR"));
bool fromEnv = !xdgRuntimeDir.isEmpty();
if (xdgRuntimeDir.isEmpty() || !checkXdgRuntimeDir(xdgRuntimeDir)) {
// environment variable not set or is set to something unsuitable
const uint myUid = uint(geteuid());
const QString userName = QFileSystemEngine::resolveUserName(myUid);
xdgRuntimeDir = QDir::tempPath() + "/runtime-"_L1 + userName;
if (!fromEnv) {
#ifndef Q_OS_WASM
qWarning("QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '%ls'", qUtf16Printable(xdgRuntimeDir));
#endif
}
if (!checkXdgRuntimeDir(xdgRuntimeDir))
xdgRuntimeDir.clear();
}
return xdgRuntimeDir;
}
default:
break;
}
#if QT_CONFIG(regularexpression)
// http://www.freedesktop.org/wiki/Software/xdg-user-dirs
QString xdgConfigHome = QFile::decodeName(qgetenv("XDG_CONFIG_HOME"));
if (xdgConfigHome.isEmpty())
xdgConfigHome = QDir::homePath() + "/.config"_L1;
QFile file(xdgConfigHome + "/user-dirs.dirs"_L1);
const QLatin1StringView key = xdg_key_name(type);
if (!key.isEmpty() && !isTestModeEnabled() && file.open(QIODevice::ReadOnly)) {
QTextStream stream(&file);
// Only look for lines like: XDG_DESKTOP_DIR="$HOME/Desktop"
static const QRegularExpression exp(u"^XDG_(.*)_DIR=(.*)$"_s);
QString result;
while (!stream.atEnd()) {
const QString &line = stream.readLine();
QRegularExpressionMatch match = exp.match(line);
if (match.hasMatch() && match.capturedView(1) == key) {
QStringView value = match.capturedView(2);
if (value.size() > 2
&& value.startsWith(u'\"')
&& value.endsWith(u'\"'))
value = value.mid(1, value.size() - 2);
// value can start with $HOME
if (value.startsWith("$HOME"_L1))
result = QDir::homePath() + value.mid(5);
else
result = value.toString();
if (result.size() > 1 && result.endsWith(u'/'))
result.chop(1);
}
}
if (!result.isNull())
return result;
}
#endif // QT_CONFIG(regularexpression)
QString path;
switch (type) {
case DesktopLocation:
path = QDir::homePath() + "/Desktop"_L1;
break;
case DocumentsLocation:
path = QDir::homePath() + "/Documents"_L1;
break;
case PicturesLocation:
path = QDir::homePath() + "/Pictures"_L1;
break;
case FontsLocation:
path = writableLocation(GenericDataLocation) + "/fonts"_L1;
break;
case MusicLocation:
path = QDir::homePath() + "/Music"_L1;
break;
case MoviesLocation:
path = QDir::homePath() + "/Videos"_L1;
break;
case DownloadLocation:
path = QDir::homePath() + "/Downloads"_L1;
break;
case ApplicationsLocation:
path = writableLocation(GenericDataLocation) + "/applications"_L1;
break;
case PublicShareLocation:
path = QDir::homePath() + "/Public"_L1;
break;
case TemplatesLocation:
path = QDir::homePath() + "/Templates"_L1;
break;
default:
break;
}
return path;
}
static QStringList dirsList(const QString &xdgEnvVar)
{
QStringList dirs;
// http://standards.freedesktop.org/basedir-spec/latest/
// Normalize paths, skip relative paths (the spec says relative paths
// should be ignored)
for (const auto dir : qTokenize(xdgEnvVar, u':'))
if (dir.startsWith(u'/'))
dirs.push_back(QDir::cleanPath(dir.toString()));
// Remove duplicates from the list, there's no use for duplicated
// paths in XDG_DATA_DIRS - if it's not found in the given
// directory the first time, it won't be there the second time.
// Plus duplicate paths causes problems for example for mimetypes,
// where duplicate paths here lead to duplicated mime types returned
// for a file, eg "text/plain,text/plain" instead of "text/plain"
dirs.removeDuplicates();
return dirs;
}
static QStringList xdgDataDirs()
{
// http://standards.freedesktop.org/basedir-spec/latest/
QString xdgDataDirsEnv = QFile::decodeName(qgetenv("XDG_DATA_DIRS"));
QStringList dirs = dirsList(xdgDataDirsEnv);
if (dirs.isEmpty())
dirs = QStringList{u"/usr/local/share"_s, u"/usr/share"_s};
return dirs;
}
static QStringList xdgConfigDirs()
{
QStringList dirs;
// http://standards.freedesktop.org/basedir-spec/latest/
const QString xdgConfigDirs = QFile::decodeName(qgetenv("XDG_CONFIG_DIRS"));
if (xdgConfigDirs.isEmpty())
dirs.append(QString::fromLatin1("/etc/xdg"));
else
dirs = xdgConfigDirs.split(u':');
return dirs;
}
QStringList QStandardPaths::standardLocations(StandardLocation type)
{
QStringList dirs;
switch (type) {
case ConfigLocation:
case GenericConfigLocation:
dirs = xdgConfigDirs();
break;
case AppConfigLocation:
dirs = xdgConfigDirs();
for (int i = 0; i < dirs.size(); ++i)
appendOrganizationAndApp(dirs[i]);
break;
case GenericDataLocation:
dirs = xdgDataDirs();
break;
case ApplicationsLocation:
dirs = xdgDataDirs();
for (int i = 0; i < dirs.size(); ++i)
dirs[i].append("/applications"_L1);
break;
case AppDataLocation:
case AppLocalDataLocation:
dirs = xdgDataDirs();
for (int i = 0; i < dirs.size(); ++i)
appendOrganizationAndApp(dirs[i]);
break;
case FontsLocation:
dirs += QDir::homePath() + "/.fonts"_L1;
dirs += xdgDataDirs();
for (int i = 1; i < dirs.size(); ++i)
dirs[i].append("/fonts"_L1);
break;
default:
break;
}
const QString localDir = writableLocation(type);
dirs.prepend(localDir);
return dirs;
}
QT_END_NAMESPACE
#endif // QT_NO_STANDARDPATHS