File indexing completed on 2024-06-02 05:34:38

0001 /*
0002  *   SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy <mariamfahmy66@gmail.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "RpmOstreeResource.h"
0008 #include "RpmOstreeBackend.h"
0009 
0010 #include <appstream/AppStreamIntegration.h>
0011 
0012 #include <KLocalizedString>
0013 #include <KOSRelease>
0014 
0015 #include <ostree-repo.h>
0016 #include <ostree.h>
0017 
0018 RpmOstreeResource::RpmOstreeResource(const QVariantMap &map, RpmOstreeBackend *parent)
0019     : AbstractResource(parent)
0020     // All available deployments are by definition already installed
0021     , m_state(AbstractResource::Installed)
0022 {
0023 #ifdef QT_DEBUG
0024     qDebug() << "rpm-ostree-backend: Creating deployments from:";
0025     QMapIterator<QString, QVariant> iter(map);
0026     while (iter.hasNext()) {
0027         iter.next();
0028         qDebug() << "rpm-ostree-backend: " << iter.key() << ": " << iter.value();
0029     }
0030     qDebug() << "";
0031 #endif
0032 
0033     // Get as much as possible from rpm-ostree
0034     m_osname = map.value(QLatin1String("osname")).toString();
0035 
0036     // Look for the base-checksum first. This is the case where we have changes layered
0037     m_checksum = map.value(QLatin1String("base-checksum")).toString();
0038     if (m_checksum.isEmpty()) {
0039         // If empty, look for the regular checksum (no layered changes)
0040         m_checksum = map.value(QLatin1String("checksum")).toString();
0041     }
0042 
0043     // Look for the base-version first. This is the case where we have changes layered
0044     m_version = map.value(QLatin1String("base-version")).toString();
0045     if (m_version.isEmpty()) {
0046         // If empty, look for the regular version (no layered changes)
0047         m_version = map.value(QLatin1String("version")).toString();
0048     }
0049 
0050     // Look for the base-timestamp first. This is the case where we have changes layered
0051     auto timestamp = map.value(QLatin1String("base-timestamp")).toULongLong();
0052     if (timestamp == 0) {
0053         // If "empty", look for the regular timestamp (no layered changes)
0054         timestamp = map.value(QLatin1String("timestamp")).toULongLong();
0055     }
0056     if (timestamp == 0) {
0057         // If it's still empty, set an "empty" date
0058         m_timestamp = QDate();
0059     } else {
0060         // Otherwise, convert the timestamp to a date
0061         m_timestamp = QDateTime::fromSecsSinceEpoch(timestamp).date();
0062     }
0063 
0064     m_pinned = map.value(QLatin1String("pinned")).toBool();
0065     m_pending = map.value(QLatin1String("staged")).toBool();
0066     m_booted = map.value(QLatin1String("booted")).toBool();
0067 
0068     if (m_booted) {
0069         // We can directly read the pretty name & variant from os-release
0070         // information if this is the currently booted deployment.
0071         auto osrelease = AppStreamIntegration::global()->osRelease();
0072         m_name = osrelease->name();
0073         m_variant = osrelease->variant();
0074         // Also extract the version if we could not find it earlier
0075         if (m_version.isEmpty()) {
0076             m_version = osrelease->versionId();
0077         }
0078     }
0079 
0080     // Look for "classic" ostree origin format first
0081     QString origin = map.value(QLatin1String("origin")).toString();
0082     if (!origin.isEmpty()) {
0083         m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::Classic, origin));
0084         if (!m_ostreeFormat->isValid()) {
0085             // This should never happen
0086             qWarning() << "rpm-ostree-backend: Invalid origin for classic ostree format:" << origin;
0087         }
0088     } else {
0089         // Then look for OCI container format
0090         origin = map.value(QLatin1String("container-image-reference")).toString();
0091         if (!origin.isEmpty()) {
0092             m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::OCI, origin));
0093             if (!m_ostreeFormat->isValid()) {
0094                 // This should never happen
0095                 qWarning() << "rpm-ostree-backend: Invalid reference for OCI container ostree format:" << origin;
0096             }
0097         } else {
0098             // This should never happen
0099             m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::Unknown, {}));
0100             qWarning() << "rpm-ostree-backend: Could not find a valid remote for this deployment:" << m_checksum;
0101         }
0102     }
0103 
0104     // Use ostree as tld for all ostree deployments and differentiate between them with the
0105     // remote/repo, ref/tag and commit..
0106     // Example: ostree.fedora.fedora-34-x86-64-kinoite.abcd1234567890
0107     // Example: ostree.quay.io-fedora-ostree-desktops-kinoite.abcd1234567890
0108     // https://freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-id-generic
0109     if (m_ostreeFormat->isClassic()) {
0110         m_appstreamid = m_ostreeFormat->remote() + QLatin1String(".") + m_ostreeFormat->ref();
0111     } else if (m_ostreeFormat->isOCI()) {
0112         m_appstreamid = m_ostreeFormat->repo() + QLatin1String(".") + m_ostreeFormat->tag();
0113     } else {
0114         m_appstreamid = QString();
0115     }
0116     m_appstreamid = QStringLiteral("ostree.") + m_appstreamid.replace(QLatin1Char('/'), QLatin1Char('-')).replace(QLatin1Char('_'), QLatin1Char('-'))
0117         + QLatin1String(".") + m_checksum;
0118 #ifdef QT_DEBUG
0119     qInfo() << "rpm-ostree-backend: Found deployment:" << m_appstreamid;
0120 #endif
0121 
0122     // Replaced & added packages
0123     m_requested_base_local_replacements = map.value(QLatin1String("requested-base-local-replacements")).toStringList();
0124     m_requested_base_removals = map.value(QLatin1String("requested-base-removals")).toStringList();
0125     m_requested_local_packages = map.value(QLatin1String("requested-local-packages")).toStringList();
0126     m_requested_modules = map.value(QLatin1String("requested-modules")).toStringList();
0127     m_requested_packages = map.value(QLatin1String("requested-packages")).toStringList();
0128     m_requested_base_local_replacements.sort();
0129     m_requested_base_removals.sort();
0130     m_requested_local_packages.sort();
0131     m_requested_modules.sort();
0132     m_requested_packages.sort();
0133 
0134     // TODO: Extract signature information
0135 }
0136 
0137 bool RpmOstreeResource::setNewMajorVersion(const QString &newMajorVersion)
0138 {
0139     if (!m_ostreeFormat->isValid()) {
0140         // Only operate on valid origin format
0141         return false;
0142     }
0143 
0144     // This check mostly makes sense for the classic Ostree format. Skip most of it for the
0145     // OCI  format case: it will fail later if the container tag does not exist.
0146     if (m_ostreeFormat->isOCI()) {
0147         // If we are using the latest tag then it means that we are not following a specific
0148         // major release and thus we don't need to rebase: it will automatically happen once
0149         // the latest tag points to a version build with the new major release.
0150         if (m_ostreeFormat->tag() == QLatin1String("latest")) {
0151             return false;
0152         }
0153 
0154         // Set the new major version
0155         m_nextMajorVersion = newMajorVersion;
0156         // Replace the current version in the container tag by the new major version to find
0157         // the new tag to rebase to. This assumes that container tag names are lowercase.
0158         QString currentVersion = AppStreamIntegration::global()->osRelease()->versionId();
0159         m_nextMajorVersionRef = m_ostreeFormat->tag().replace(currentVersion, newMajorVersion.toLower(), Qt::CaseInsensitive);
0160         return true;
0161     }
0162 
0163     // Assume we're using the classic format from now on
0164     if (!m_ostreeFormat->isClassic()) {
0165         // Only operate on valid origin format
0166         return false;
0167     }
0168 
0169     // Fetch the list of refs available on the remote for the deployment
0170     g_autoptr(GFile) path = g_file_new_for_path("/ostree/repo");
0171     g_autoptr(OstreeRepo) repo = ostree_repo_new(path);
0172     if (repo == NULL) {
0173         qWarning() << "rpm-ostree-backend: Could not find ostree repo:" << path;
0174         return false;
0175     }
0176 
0177     g_autoptr(GError) err = NULL;
0178     gboolean res = ostree_repo_open(repo, NULL, &err);
0179     if (!res) {
0180         qWarning() << "rpm-ostree-backend: Could not open ostree repo:" << path;
0181         return false;
0182     }
0183 
0184     g_autoptr(GHashTable) refs;
0185     QByteArray rem = m_ostreeFormat->remote().toLocal8Bit();
0186     res = ostree_repo_remote_list_refs(repo, rem.data(), &refs, NULL, &err);
0187     if (!res) {
0188         qWarning() << "rpm-ostree-backend: Could not get the list of refs for ostree repo:" << path;
0189         return false;
0190     }
0191 
0192     // Replace the current version in current branch by the new major version to find the
0193     // new branch. This assumes that ostree branch names are lowercase.
0194     QString currentVersion = AppStreamIntegration::global()->osRelease()->versionId();
0195     QString newVersionBranch = m_ostreeFormat->ref().replace(currentVersion, newMajorVersion.toLower(), Qt::CaseInsensitive);
0196 
0197     // Iterate over the remote refs to verify that the new verions has a branch available
0198     GHashTableIter iter;
0199     gpointer key, value;
0200     g_hash_table_iter_init(&iter, refs);
0201     while (g_hash_table_iter_next(&iter, &key, &value)) {
0202         auto ref = QString::fromUtf8((char *)key);
0203         if (ref == newVersionBranch) {
0204             m_nextMajorVersion = newMajorVersion;
0205             m_nextMajorVersionRef = newVersionBranch;
0206             return true;
0207         }
0208     }
0209 
0210     // If we reach here, it means that we could not find a matching branch. This
0211     // is unexpected and we should inform the user.
0212     qWarning() << "rpm-ostree-backend: Could not find a remote ref for the new major version in ostree repo";
0213     return false;
0214 }
0215 
0216 QString RpmOstreeResource::availableVersion() const
0217 {
0218     return m_newVersion;
0219 }
0220 
0221 QString RpmOstreeResource::version()
0222 {
0223     return m_version;
0224 }
0225 
0226 void RpmOstreeResource::setNewVersion(const QString &newVersion)
0227 {
0228     m_newVersion = newVersion;
0229 }
0230 
0231 QString RpmOstreeResource::getNewVersion() const
0232 {
0233     return m_newVersion;
0234 }
0235 
0236 QString RpmOstreeResource::getNextMajorVersion() const
0237 {
0238     return m_nextMajorVersion;
0239 }
0240 
0241 QString RpmOstreeResource::getNextMajorVersionRef() const
0242 {
0243     return m_nextMajorVersionRef;
0244 }
0245 
0246 QString RpmOstreeResource::appstreamId() const
0247 {
0248     return m_appstreamid;
0249 }
0250 
0251 bool RpmOstreeResource::canExecute() const
0252 {
0253     return false;
0254 }
0255 
0256 QVariant RpmOstreeResource::icon() const
0257 {
0258     return QStringLiteral("application-x-rpm");
0259 }
0260 
0261 QString RpmOstreeResource::installedVersion() const
0262 {
0263     return m_version;
0264 }
0265 
0266 QUrl RpmOstreeResource::url() const
0267 {
0268     return QUrl();
0269 }
0270 
0271 QUrl RpmOstreeResource::donationURL()
0272 {
0273     return QUrl();
0274 }
0275 
0276 QUrl RpmOstreeResource::homepage()
0277 {
0278     return QUrl(AppStreamIntegration::global()->osRelease()->homeUrl());
0279 }
0280 
0281 QUrl RpmOstreeResource::helpURL()
0282 {
0283     return QUrl(AppStreamIntegration::global()->osRelease()->documentationUrl());
0284 }
0285 
0286 QUrl RpmOstreeResource::bugURL()
0287 {
0288     return QUrl(AppStreamIntegration::global()->osRelease()->bugReportUrl());
0289 }
0290 
0291 QJsonArray RpmOstreeResource::licenses()
0292 {
0293     if (m_osname == QLatin1String("fedora")) {
0294         return {QJsonObject{{QStringLiteral("name"), i18n("GPL and other licenses")},
0295                             {QStringLiteral("url"), QStringLiteral("https://fedoraproject.org/wiki/Legal:Licenses")}}};
0296     }
0297     return {QJsonObject{{QStringLiteral("name"), i18n("Unknown")}}};
0298 }
0299 
0300 QString RpmOstreeResource::longDescription()
0301 {
0302     QString desc;
0303     if (!m_requested_packages.isEmpty()) {
0304         QTextStream(&desc) << i18n("Additional packages: ") << "\n<ul>";
0305         for (const QString &package : std::as_const(m_requested_packages)) {
0306             QTextStream(&desc) << "<li>" << package << "</li>\n";
0307         }
0308         QTextStream(&desc) << "</ul>\n";
0309     }
0310     if (!m_requested_modules.isEmpty()) {
0311         QTextStream(&desc) << i18n("Additional modules: ") << "\n<ul>";
0312         for (const QString &package : std::as_const(m_requested_modules)) {
0313             QTextStream(&desc) << "<li>" << package << "</li>\n";
0314         }
0315         QTextStream(&desc) << "</ul>\n";
0316     }
0317     if (!m_requested_local_packages.isEmpty()) {
0318         QTextStream(&desc) << i18n("Local packages: ") << "\n<ul>";
0319         for (const QString &package : std::as_const(m_requested_local_packages)) {
0320             QTextStream(&desc) << "<li>" << package << "</li>\n";
0321         }
0322         QTextStream(&desc) << "</ul>\n";
0323     }
0324     if (!m_requested_base_local_replacements.isEmpty()) {
0325         QTextStream(&desc) << i18n("Replaced packages:") << "\n<ul>";
0326         for (const QString &package : std::as_const(m_requested_base_local_replacements)) {
0327             QTextStream(&desc) << "<li>" << package << "</li>\n";
0328         }
0329         QTextStream(&desc) << "</ul>\n";
0330     }
0331     if (!m_requested_base_removals.isEmpty()) {
0332         QTextStream(&desc) << i18n("Removed packages:") << "\n<ul>";
0333         for (const QString &package : std::as_const(m_requested_base_removals)) {
0334             QTextStream(&desc) << "<li>" << package << "</li>\n";
0335         }
0336         QTextStream(&desc) << "</ul>\n";
0337     }
0338     if (m_pinned) {
0339         desc += QStringLiteral("<br/>This version is pinned and won't be automatically removed on updates.");
0340     }
0341     return desc;
0342 }
0343 
0344 QString RpmOstreeResource::name() const
0345 {
0346     return QStringLiteral("%1 %2").arg(packageName(), m_version);
0347 }
0348 
0349 QString RpmOstreeResource::origin() const
0350 {
0351     if (m_ostreeFormat->isClassic()) {
0352         if (m_ostreeFormat->remote() == QLatin1String("fedora")) {
0353             return QStringLiteral("Fedora Project");
0354         } else {
0355             return m_ostreeFormat->remote();
0356         }
0357     } else if (m_ostreeFormat->isOCI()) {
0358         return m_ostreeFormat->repo();
0359     }
0360     return i18n("Unknown");
0361 }
0362 
0363 QString RpmOstreeResource::packageName() const
0364 {
0365     if (m_osname == QLatin1String("fedora")) {
0366         return QStringLiteral("Fedora Kinoite");
0367     }
0368     return m_osname;
0369 }
0370 
0371 QString RpmOstreeResource::section()
0372 {
0373     return {};
0374 }
0375 
0376 AbstractResource::State RpmOstreeResource::state()
0377 {
0378     return m_state;
0379 }
0380 
0381 QString RpmOstreeResource::author() const
0382 {
0383     if (m_osname == QLatin1String("fedora")) {
0384         return QStringLiteral("Fedora Project");
0385     }
0386     return i18n("Unknown");
0387 }
0388 
0389 QString RpmOstreeResource::comment()
0390 {
0391     if (m_booted) {
0392         if (m_pinned) {
0393             return i18n("Currently booted version (pinned)");
0394         } else {
0395             return i18n("Currently booted version");
0396         }
0397     } else if (m_pending) {
0398         return i18n("Version that will be used after reboot");
0399     } else if (m_pinned) {
0400         return i18n("Fallback version (pinned)");
0401     }
0402     return i18n("Fallback version");
0403 }
0404 
0405 quint64 RpmOstreeResource::size()
0406 {
0407     return 0;
0408 }
0409 
0410 QString RpmOstreeResource::sizeDescription()
0411 {
0412     return QStringLiteral("Unknown");
0413 }
0414 
0415 QDate RpmOstreeResource::releaseDate() const
0416 {
0417     return m_timestamp;
0418 }
0419 
0420 void RpmOstreeResource::setState(AbstractResource::State state)
0421 {
0422     m_state = state;
0423     Q_EMIT stateChanged();
0424 }
0425 
0426 QString RpmOstreeResource::sourceIcon() const
0427 {
0428     return QStringLiteral("application-x-rpm");
0429 }
0430 
0431 QStringList RpmOstreeResource::extends() const
0432 {
0433     return {};
0434 }
0435 AbstractResource::Type RpmOstreeResource::type() const
0436 {
0437     return Technical;
0438 }
0439 
0440 bool RpmOstreeResource::isRemovable() const
0441 {
0442     // TODO: Add support for pinning, un-pinning and removing a specific
0443     // deployments. Until we have that, we consider all deployments as pinned by
0444     // default (and thus non-removable).
0445     // return !m_booted && !m_pinned;
0446     return false;
0447 }
0448 
0449 QList<PackageState> RpmOstreeResource::addonsInformation()
0450 {
0451     return QList<PackageState>();
0452 }
0453 
0454 QStringList RpmOstreeResource::categories()
0455 {
0456     return {};
0457 }
0458 
0459 bool RpmOstreeResource::isBooted()
0460 {
0461     return m_booted;
0462 }
0463 
0464 bool RpmOstreeResource::isPending()
0465 {
0466     return m_pending;
0467 }
0468 
0469 bool RpmOstreeResource::isClassic()
0470 {
0471     return m_ostreeFormat->isValid() && m_ostreeFormat->isClassic();
0472 }
0473 
0474 bool RpmOstreeResource::isOCI()
0475 {
0476     return m_ostreeFormat->isValid() && m_ostreeFormat->isOCI();
0477 }
0478 
0479 QString RpmOstreeResource::OCIUrl()
0480 {
0481     // This will fail on non-remote transports (oci, oci-archive, containers-storage) but that's
0482     // OK as we can not check for updates in those cases.
0483     if (m_ostreeFormat->isValid() && m_ostreeFormat->isOCI()) {
0484         return QLatin1String("docker://") + m_ostreeFormat->repo() + QLatin1String(":") + m_ostreeFormat->tag();
0485         ;
0486     }
0487     // Should never happen
0488     return {};
0489 }