diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -36,6 +36,7 @@ kcompositejobtest.cpp kformattest.cpp kjobtest.cpp + kosreleasetest.cpp kpluginfactorytest.cpp kpluginloadertest.cpp kpluginmetadatatest.cpp diff --git a/autotests/data/os-release b/autotests/data/os-release new file mode 100644 --- /dev/null +++ b/autotests/data/os-release @@ -0,0 +1,22 @@ +NAME="Name" +VERSION="100.5" +ID=theid +ID_LIKE="otherid otherotherid" +VERSION_CODENAME=versioncodename +VERSION_ID="500.1" +PRETTY_NAME="Pretty Name #1" +ANSI_COLOR="1;34" +CPE_NAME="cpe:/o:foo:bar:100" +HOME_URL="https://url.home" +DOCUMENTATION_URL="https://url.docs" +SUPPORT_URL="https://url.support" +BUG_REPORT_URL="https://url.bugs" +PRIVACY_POLICY_URL="https://url.privacy" +BUILD_ID="105.5" +# comment +VARIANT="Test = Edition" +BROKENLINE_SHOULD_BE_IGNORED +VARIANT_ID=test + # indented comment +LOGO=start-here-test +DEBIAN_BTS="debbugs://bugs.debian.org/" diff --git a/autotests/kosreleasetest.cpp b/autotests/kosreleasetest.cpp new file mode 100644 --- /dev/null +++ b/autotests/kosreleasetest.cpp @@ -0,0 +1,57 @@ +/* + Copyright (C) 2014-2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include + +#include "kosrelease.h" + +class KOSReleaseTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testParse() + { + KOSRelease r(QFINDTESTDATA("data/os-release")); + QCOMPARE(r.name(), QStringLiteral("Name")); + QCOMPARE(r.version(), QStringLiteral("100.5")); + QCOMPARE(r.id(), QStringLiteral("theid")); + QCOMPARE(r.idLike(), QStringList({ QStringLiteral("otherid"), QStringLiteral("otherotherid") })); + QCOMPARE(r.versionCodename(), QStringLiteral("versioncodename")); + QCOMPARE(r.versionId(), QStringLiteral("500.1")); + QCOMPARE(r.prettyName(), QStringLiteral("Pretty Name #1")); + QCOMPARE(r.ansiColor(), QStringLiteral("1;34")); + QCOMPARE(r.cpeName(), QStringLiteral("cpe:/o:foo:bar:100")); + QCOMPARE(r.homeUrl(), QStringLiteral("https://url.home")); + QCOMPARE(r.documentationUrl(), QStringLiteral("https://url.docs")); + QCOMPARE(r.supportUrl(), QStringLiteral("https://url.support")); + QCOMPARE(r.bugReportUrl(), QStringLiteral("https://url.bugs")); + QCOMPARE(r.privacyPolicyUrl(), QStringLiteral("https://url.privacy")); + QCOMPARE(r.buildId(), QStringLiteral("105.5")); + QCOMPARE(r.variant(), QStringLiteral("Test = Edition")); + QCOMPARE(r.variantId(), QStringLiteral("test")); + QCOMPARE(r.logo(), QStringLiteral("start-here-test")); + QCOMPARE(r.extraKeys(), QStringList({ QStringLiteral("DEBIAN_BTS") })); + QCOMPARE(r.extraValue(QStringLiteral("DEBIAN_BTS")), QStringLiteral("debbugs://bugs.debian.org/")); + } +}; + +QTEST_MAIN(KOSReleaseTest) + +#include "kosreleasetest.moc" diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -86,6 +86,7 @@ util/kdelibs4configmigrator.cpp util/kformat.cpp util/kformatprivate.cpp + util/kosrelease.cpp util/kshell.cpp ${kcoreaddons_OPTIONAL_SRCS} ${kcoreaddons_QM_LOADER} @@ -197,6 +198,7 @@ ecm_generate_headers(KCoreAddons_HEADERS HEADER_NAMES KFormat + KOSRelease KUser KShell Kdelibs4Migration @@ -241,6 +243,7 @@ text/ktexttohtml.h text/ktexttohtmlemoticonsinterface.h util/kformat.h + util/kosrelease.h util/kuser.h util/kshell.h util/kdelibs4migration.h diff --git a/src/lib/util/kosrelease.h b/src/lib/util/kosrelease.h new file mode 100644 --- /dev/null +++ b/src/lib/util/kosrelease.h @@ -0,0 +1,108 @@ +/* + Copyright (C) 2014-2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef KOSRELEASE_H +#define KOSRELEASE_H + +#include + +#include +#include + +/** + * @brief The OSRelease class parses /etc/os-release files + * + * https://www.freedesktop.org/software/systemd/man/os-release.html + * + * os-release is a free desktop standard for describing an operating system. + * This class parses and models os-release files. + * + * @since 5.58.0 + */ +class KCOREADDONS_EXPORT KOSRelease Q_DECL_FINAL +{ +public: + /** + * Constructs a new OSRelease instance. Parsing happens in the constructor + * and the data is not cached across instances. + * + * @note The format specification makes no assertions about trailing # + * comments being supported. They result in undefined behavior. + * + * @param filePath The path to the os-release file. By default the first + * available file of the paths specified in the os-release manpage is + * parsed. + */ + explicit KOSRelease(const QString &filePath = QString()); + ~KOSRelease(); + + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#NAME= */ + QString name() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#VERSION= */ + QString version() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#ID= */ + QString id() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#ID_LIKE= */ + QStringList idLike() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#VERSION_CODENAME= */ + QString versionCodename() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#VERSION_ID= */ + QString versionId() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#PRETTY_NAME= */ + QString prettyName() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#ANSI_COLOR= */ + QString ansiColor() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#CPE_NAME= */ + QString cpeName() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#HOME_URL= */ + QString homeUrl() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#HOME_URL= */ + QString documentationUrl() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#HOME_URL= */ + QString supportUrl() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#HOME_URL= */ + QString bugReportUrl() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#HOME_URL= */ + QString privacyPolicyUrl() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#BUILD_ID= */ + QString buildId() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#VARIANT= */ + QString variant() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#VARIANT_ID= */ + QString variantId() const; + /** @see https://www.freedesktop.org/software/systemd/man/os-release.html#LOGO= */ + QString logo() const; + + /** + * Extra keys are keys that are unknown or specified by a vendor. + */ + QStringList extraKeys() const; + + /** Extra values are values assoicated with keys that are unknown. */ + QString extraValue(const QString &key) const; + +private: + Q_DISABLE_COPY(KOSRelease) + + class Private; + Private *const d = nullptr; +}; + +#endif // KOSRELEASE_H diff --git a/src/lib/util/kosrelease.cpp b/src/lib/util/kosrelease.cpp new file mode 100644 --- /dev/null +++ b/src/lib/util/kosrelease.cpp @@ -0,0 +1,306 @@ +/* + Copyright (C) 2014-2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "kosrelease.h" + +#include +#include + +#include "kcoreaddons_debug.h" +#include "kshell.h" + +// Sets a QString var +static void setVar(QString *var, const QString &value) +{ + // Values may contain quotation marks, strip them as we have no use for them. + KShell::Errors error; + QStringList args = KShell::splitArgs(value, KShell::NoOptions, &error); + if (error != KShell::NoError) { // Failed to parse. + return; + } + *var = args.join(QLatin1Char(' ')); +} + +// Sets a QStringList var (i.e. splits a string value) +static void setVar(QStringList *var, const QString &value) +{ + // Instead of passing the verbatim value we manually strip any initial quotes + // and then run it through KShell. At this point KShell will actually split + // by spaces giving us the final QStringList. + // NOTE: Splitting like this does not actually allow escaped substrings to + // be handled correctly, so "kitteh \"french fries\"" would result in + // three list entries. I'd argue that if someone makes an id like that + // they are at fault for the bogus parsing here though as id explicitly + // is required to not contain spaces even if more advanced shell escaping + // is also allowed... + QString value_ = value; + if (value_.at(0) == QLatin1Char('"') && value_.at(value_.size()-1) == QLatin1Char('"')) { + value_.remove(0, 1); + value_.remove(-1, 1); + } + KShell::Errors error; + QStringList args = KShell::splitArgs(value_, KShell::NoOptions, &error); + if (error != KShell::NoError) { // Failed to parse. + return; + } + *var = args; +} + +static QStringList splitEntry(const QString &line) +{ + QStringList list; + const int separatorIndex = line.indexOf(QLatin1Char('=')); + list << line.mid(0, separatorIndex); + if (separatorIndex != -1) { + list << line.mid(separatorIndex + 1, -1); + } + return list; +} + +static QString defaultFilePath() +{ + if (QFile::exists(QStringLiteral("/etc/os-release"))) { + return QStringLiteral("/etc/os-release"); + } else if (QFile::exists(QStringLiteral("/usr/lib/os-release"))) { + return QStringLiteral("/usr/lib/os-release"); + } else { + return QString(); + } +} + +class Q_DECL_HIDDEN KOSRelease::Private +{ +public: + Private(QString filePath) + : name(QStringLiteral("Linux")) + , id(QStringLiteral("linux")) + , prettyName(QStringLiteral("Linux")) + { + // Default values for non-optional fields set above ^. + + QHash stringHash = { + { QStringLiteral("NAME"), &name }, + { QStringLiteral("VERSION"), &version }, + { QStringLiteral("ID"), &id }, + // idLike is not a QString, special handling below! + { QStringLiteral("VERSION_CODENAME"), &versionCodename }, + { QStringLiteral("VERSION_ID"), &versionId }, + { QStringLiteral("PRETTY_NAME"), &prettyName }, + { QStringLiteral("ANSI_COLOR"), &ansiColor }, + { QStringLiteral("CPE_NAME"), &cpeName }, + { QStringLiteral("HOME_URL"), &homeUrl }, + { QStringLiteral("DOCUMENTATION_URL"), &documentationUrl }, + { QStringLiteral("SUPPORT_URL"), &supportUrl }, + { QStringLiteral("BUG_REPORT_URL"), &bugReportUrl }, + { QStringLiteral("PRIVACY_POLICY_URL"), &privacyPolicyUrl }, + { QStringLiteral("BUILD_ID"), &buildId }, + { QStringLiteral("VARIANT"), &variant }, + { QStringLiteral("VARIANT_ID"), &variantId }, + { QStringLiteral("LOGO"), &logo } + }; + + if (filePath.isEmpty()) { + filePath = defaultFilePath(); + } + if (filePath.isEmpty()) { + qCWarning(KCOREADDONS_DEBUG) << "Failed to find os-release file!"; + return; + } + + QFile file(filePath); + // NOTE: The os-release specification defines default values for specific + // fields which means that even if we can not read the os-release file + // we have sort of expected default values to use. + // TODO: it might still be handy to indicate to the outside whether + // fallback values are being used or not. + file.open(QIODevice::ReadOnly | QIODevice::Text); + QString line; + QStringList parts; + while (!file.atEnd()) { + // Trimmed to handle indented comment lines properly + line = QString::fromLatin1(file.readLine()).trimmed(); + + if (line.startsWith(QLatin1Char('#'))) { + // Comment line + // Lines beginning with "#" shall be ignored as comments. + continue; + } + + parts = splitEntry(line); + + if (parts.size() != 2) { + // Line has no =, must be invalid. + qCDebug(KCOREADDONS_DEBUG) << "Unexpected/invalid os-release line:" << line; + continue; + } + + QString key = parts.at(0); + QString value = parts.at(1).trimmed(); + + if (QString *var = stringHash.value(key, nullptr)) { + setVar(var, value); + continue; + } + + // ID_LIKE is a list and parsed as such (rather than a QString). + if (key == QLatin1String("ID_LIKE")) { + setVar(&idLike, value); + continue; + } + + // os-release explicitly allows for vendor specific additions, we'll + // collect them as strings and exposes them as "extras". + QString parsedValue; + setVar(&parsedValue, value); + extras.insert(key, parsedValue); + } + } + + QString name; + QString version; + QString id; + QStringList idLike; + QString versionCodename; + QString versionId; + QString prettyName; + QString ansiColor; + QString cpeName; + QString homeUrl; + QString documentationUrl; + QString supportUrl; + QString bugReportUrl; + QString privacyPolicyUrl; + QString buildId; + QString variant; + QString variantId; + QString logo; + + QHash extras; +}; + +KOSRelease::KOSRelease(const QString &filePath) + : d(new Private(filePath)) +{ +} + +KOSRelease::~KOSRelease() +{ + delete d; +} + +QString KOSRelease::name() const +{ + return d->name; +} + +QString KOSRelease::version() const +{ + return d->version; +} + +QString KOSRelease::id() const +{ + return d->id; +} + +QStringList KOSRelease::idLike() const +{ + return d->idLike; +} + +QString KOSRelease::versionCodename() const +{ + return d->versionCodename; +} + +QString KOSRelease::versionId() const +{ + return d->versionId; +} + +QString KOSRelease::prettyName() const +{ + return d->prettyName; +} + +QString KOSRelease::ansiColor() const +{ + return d->ansiColor; +} + +QString KOSRelease::cpeName() const +{ + return d->cpeName; +} + +QString KOSRelease::homeUrl() const +{ + return d->homeUrl; +} + +QString KOSRelease::documentationUrl() const +{ + return d->documentationUrl; +} + +QString KOSRelease::supportUrl() const +{ + return d->supportUrl; +} + +QString KOSRelease::bugReportUrl() const +{ + return d->bugReportUrl; +} + +QString KOSRelease::privacyPolicyUrl() const +{ + return d->privacyPolicyUrl; +} + +QString KOSRelease::buildId() const +{ + return d->buildId; +} + +QString KOSRelease::variant() const +{ + return d->variant; +} + +QString KOSRelease::variantId() const +{ + return d->variantId; +} + +QString KOSRelease::logo() const +{ + return d->logo; +} + +QStringList KOSRelease::extraKeys() const +{ + return d->extras.keys(); +} + +QString KOSRelease::extraValue(const QString &key) const +{ + return d->extras.value(key); +}