1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 1999, 2007 David Faure <faure@kde.org>
4 SPDX-FileCopyrightText: 1999 Waldo Bastian <bastian@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-only
7*/
8
9#include "kbuildmimetypefactory_p.h"
10#include "kbuildservicefactory_p.h"
11#include "kbuildservicegroupfactory_p.h"
12#include "ksycoca.h"
13
14#include "ksycocadict_p.h"
15#include "sycocadebug.h"
16#include <KDesktopFile>
17
18#include <QDebug>
19#include <QDir>
20#include <QMimeDatabase>
21
22#include <QStandardPaths>
23#include <kmimetypefactory_p.h>
24#include <kservice_p.h>
25
26KBuildServiceFactory::KBuildServiceFactory(KBuildMimeTypeFactory *mimeTypeFactory)
27 : KServiceFactory(mimeTypeFactory->sycoca())
28 , m_nameMemoryHash()
29 , m_relNameMemoryHash()
30 , m_menuIdMemoryHash()
31 , m_dupeDict()
32 , m_mimeTypeFactory(mimeTypeFactory)
33{
34 m_nameDict = new KSycocaDict();
35 m_relNameDict = new KSycocaDict();
36 m_menuIdDict = new KSycocaDict();
37}
38
39KBuildServiceFactory::~KBuildServiceFactory()
40{
41}
42
43KService::Ptr KBuildServiceFactory::findServiceByDesktopName(const QString &name)
44{
45 return m_nameMemoryHash.value(key: name);
46}
47
48KService::Ptr KBuildServiceFactory::findServiceByDesktopPath(const QString &name)
49{
50 return m_relNameMemoryHash.value(key: name);
51}
52
53KService::Ptr KBuildServiceFactory::findServiceByMenuId(const QString &menuId)
54{
55 return m_menuIdMemoryHash.value(key: menuId);
56}
57
58KSycocaEntry *KBuildServiceFactory::createEntry(const QString &file) const
59{
60 const QStringView name = QStringView(file).mid(pos: file.lastIndexOf(c: QLatin1Char('/')) + 1);
61
62 // Is it a .desktop file?
63 if (name.endsWith(s: QLatin1String(".desktop"))) {
64 // qCDebug(SYCOCA) << file;
65
66 Q_ASSERT(QDir::isAbsolutePath(file));
67 KService *serv = new KService(file);
68
69 // qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath();
70 // Note that the menuId will be set by the vfolder_menu.cpp code just after
71 // createEntry returns.
72
73 if (serv->isValid() && !serv->isDeleted()) {
74 // qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath() << "storageId=" << serv->storageId();
75 return serv;
76 } else {
77 if (!serv->isDeleted()) {
78 qCWarning(SYCOCA) << "Invalid Service : " << file;
79 }
80 delete serv;
81 return nullptr;
82 }
83 } // TODO else if a Windows application, new KService(name, exec, icon)
84 return nullptr;
85}
86
87void KBuildServiceFactory::saveHeader(QDataStream &str)
88{
89 KSycocaFactory::saveHeader(str);
90
91 str << qint32(m_nameDictOffset);
92 str << qint32(m_relNameDictOffset);
93 str << qint32(m_offerListOffset);
94 str << qint32(m_menuIdDictOffset);
95}
96
97void KBuildServiceFactory::save(QDataStream &str)
98{
99 KSycocaFactory::save(str);
100
101 m_nameDictOffset = str.device()->pos();
102 m_nameDict->save(str);
103
104 m_relNameDictOffset = str.device()->pos();
105 m_relNameDict->save(str);
106
107 saveOfferList(str);
108
109 m_menuIdDictOffset = str.device()->pos();
110 m_menuIdDict->save(str);
111
112 qint64 endOfFactoryData = str.device()->pos();
113
114 // Update header (pass #3)
115 saveHeader(str);
116
117 // Seek to end.
118 str.device()->seek(pos: endOfFactoryData);
119}
120
121void KBuildServiceFactory::collectInheritedServices()
122{
123 // For each MIME type, go up the parent MIME type chains and collect offers.
124 // For "removed associations" to work, we can't just grab everything from all parents.
125 // We need to process parents before children, hence the recursive call in
126 // collectInheritedServices(mime) and the QSet to process a given parent only once.
127 QSet<QString> visitedMimes;
128 const auto lst = m_mimeTypeFactory->allMimeTypes();
129 for (const QString &mimeType : lst) {
130 collectInheritedServices(mime: mimeType, visitedMimes);
131 }
132}
133
134void KBuildServiceFactory::collectInheritedServices(const QString &mimeTypeName, QSet<QString> &visitedMimes)
135{
136 if (visitedMimes.contains(value: mimeTypeName)) {
137 return;
138 }
139 visitedMimes.insert(value: mimeTypeName);
140
141 QMimeDatabase db;
142 const QMimeType qmime = db.mimeTypeForName(nameOrAlias: mimeTypeName);
143 const auto lst = qmime.parentMimeTypes();
144 for (QString parentMimeType : lst) {
145 // Workaround issue in shared-mime-info and/or Qt, which sometimes return an alias as parent
146 parentMimeType = db.mimeTypeForName(nameOrAlias: parentMimeType).name();
147
148 collectInheritedServices(mimeTypeName: parentMimeType, visitedMimes);
149
150 const QList<KServiceOffer> &offers = m_offerHash.offersFor(serviceType: parentMimeType);
151 for (const auto &serviceOffer : offers) {
152 if (!m_offerHash.hasRemovedOffer(serviceType: mimeTypeName, service: serviceOffer.service())) {
153 KServiceOffer offer(serviceOffer);
154 offer.setMimeTypeInheritanceLevel(offer.mimeTypeInheritanceLevel() + 1);
155 // qCDebug(SYCOCA) << "INHERITANCE: Adding service" << (*itserv).service()->entryPath() << "to" << mimeTypeName << "mimeTypeInheritanceLevel="
156 // << mimeTypeInheritanceLevel;
157 m_offerHash.addServiceOffer(serviceType: mimeTypeName, offer);
158 }
159 }
160 }
161}
162
163void KBuildServiceFactory::postProcessServices()
164{
165 // By doing all this here rather than in addEntry (and removing when replacing
166 // with local override), we only do it for the final applications.
167 // Note that this also affects resolution of the by-desktop-name lookup,
168 // as name resolution is only performed *after* all the duplicates (based on
169 // storage ID) have been removed.
170
171 // For every service...
172 for (auto itserv = m_entryDict->cbegin(), endIt = m_entryDict->cend(); itserv != endIt; ++itserv) {
173 KSycocaEntry::Ptr entry = itserv.value();
174 KService::Ptr service(static_cast<KService *>(entry.data()));
175
176 const QString name = service->desktopEntryName();
177 KService::Ptr dup = m_nameMemoryHash.value(key: name);
178 if (dup) {
179 // The rule is that searching for the desktop name "foo" should find
180 // the desktop file with the storage id "foo.desktop" before it
181 // finds "bar/foo.desktop" (or "bar-foo.desktop").
182 // "bar/foo.desktop" and "baz/foo.desktop" are arbitrarily ordered
183 // (in practice, the one later in the alphabet wins).
184 if (dup->storageId().endsWith(s: service->storageId())) {
185 // allow dup to be overridden
186 m_nameDict->remove(key: name);
187 dup = nullptr;
188 }
189 }
190 if (!dup) {
191 m_nameDict->add(key: name, payload: entry);
192 m_nameMemoryHash.insert(key: name, value: service);
193 }
194
195 const QString relName = service->entryPath();
196 // qCDebug(SYCOCA) << "adding service" << service.data() << "isApp=" << service->isApplication() << "menuId=" << service->menuId() << "name=" << name <<
197 // "relName=" << relName;
198 m_relNameDict->add(key: relName, payload: entry);
199 m_relNameMemoryHash.insert(key: relName, value: service); // for KMimeAssociations
200
201 const QString menuId = service->menuId();
202 if (!menuId.isEmpty()) { // empty for services, non-empty for applications
203 m_menuIdDict->add(key: menuId, payload: entry);
204 m_menuIdMemoryHash.insert(key: menuId, value: service); // for KMimeAssociations
205 }
206 }
207 populateServiceTypes();
208}
209
210void KBuildServiceFactory::populateServiceTypes()
211{
212 QMimeDatabase db;
213 // For every service...
214 for (auto servIt = m_entryDict->cbegin(), endIt = m_entryDict->cend(); servIt != endIt; ++servIt) {
215 KService::Ptr service(static_cast<KService *>(servIt.value().data()));
216 const bool hidden = !service->showInCurrentDesktop();
217
218 const auto mimeTypes = service->d_func()->m_mimeTypes;
219
220 // Add this service to all its MIME types
221 // Don't cache count(), it can change during iteration! (we can't use an iterator-based loop
222 // here the container could get reallocated which would invalidate iterators)
223 for (int i = 0; i < mimeTypes.count(); ++i) {
224 const QString &mimeName = mimeTypes.at(i);
225
226 if (hidden) {
227 continue;
228 }
229
230 KServiceOffer offer(service, 1 /* preference; always 1 here, may be higher based on KMimeAssociations */, 0);
231 QMimeType mime = db.mimeTypeForName(nameOrAlias: mimeName);
232 if (!mime.isValid()) {
233 if (mimeName.startsWith(s: QLatin1String("x-scheme-handler/"))) {
234 // Create those on demand
235 m_mimeTypeFactory->createFakeMimeType(name: mimeName);
236 m_offerHash.addServiceOffer(serviceType: mimeName, offer);
237 } else {
238 qCDebug(SYCOCA) << service->entryPath() << "specifies undefined MIME type/servicetype" << mimeName;
239 // technically we could call addServiceOffer here, 'mime' isn't used. But it
240 // would be useless, since we have no MIME type entry where to write the offers offset.
241 continue;
242 }
243 } else {
244 bool shouldAdd = true;
245 const auto lst = service->mimeTypes();
246
247 for (const QString &otherType : lst) {
248 // Skip derived types if the base class is listed (#321706)
249 if (mimeName != otherType && mime.inherits(mimeTypeName: otherType)) {
250 // But don't skip aliases (they got resolved into mime.name() already, but don't let two aliases cancel out)
251 if (db.mimeTypeForName(nameOrAlias: otherType).name() != mime.name()) {
252 // qCDebug(SYCOCA) << "Skipping" << mime.name() << "because of" << otherType << "(canonical" << db.mimeTypeForName(otherType) <<
253 // ") while parsing" << service->entryPath();
254 shouldAdd = false;
255 }
256 }
257 }
258 if (shouldAdd) {
259 // qCDebug(SYCOCA) << "Adding service" << service->entryPath() << "to" << mime.name();
260 m_offerHash.addServiceOffer(serviceType: mime.name(), offer); // mime.name() so that we resolve aliases
261 }
262 }
263 }
264 }
265
266 // Read user preferences (added/removed associations) and add/remove serviceoffers to m_offerHash
267 KMimeAssociations mimeAssociations(m_offerHash, this);
268 mimeAssociations.parseAllMimeAppsList();
269
270 // Now for each MIME type, collect services from parent MIME types
271 collectInheritedServices();
272
273 // Now collect the offsets into the (future) offer list
274 // The loops look very much like the ones in saveOfferList obviously.
275 int offersOffset = 0;
276 const int offerEntrySize = sizeof(qint32) * 4; // four qint32s, see saveOfferList.
277
278 const auto &offerHash = m_offerHash.serviceTypeData();
279 auto it = offerHash.constBegin();
280 const auto end = offerHash.constEnd();
281 for (; it != end; ++it) {
282 const QString stName = it.key();
283 const ServiceTypeOffersData offersData = it.value();
284 const int numOffers = offersData.offers.count();
285
286 KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(name: stName);
287 if (entry) {
288 entry->setServiceOffersOffset(offersOffset);
289 offersOffset += offerEntrySize * numOffers;
290 } else if (stName.startsWith(s: QLatin1String("x-scheme-handler/"))) {
291 // Create those on demand
292 entry = m_mimeTypeFactory->createFakeMimeType(name: stName);
293 entry->setServiceOffersOffset(offersOffset);
294 offersOffset += offerEntrySize * numOffers;
295 } else {
296 if (stName.isEmpty()) {
297 qCDebug(SYCOCA) << "Empty service type";
298 } else {
299 qCWarning(SYCOCA) << "Service type not found:" << stName;
300 }
301 }
302 }
303}
304
305void KBuildServiceFactory::saveOfferList(QDataStream &str)
306{
307 m_offerListOffset = str.device()->pos();
308 // qCDebug(SYCOCA) << "Saving offer list at offset" << m_offerListOffset;
309
310 const auto &offerHash = m_offerHash.serviceTypeData();
311 auto it = offerHash.constBegin();
312 const auto end = offerHash.constEnd();
313 for (; it != end; ++it) {
314 const QString stName = it.key();
315 const ServiceTypeOffersData offersData = it.value();
316 QList<KServiceOffer> offers = offersData.offers;
317 std::stable_sort(first: offers.begin(), last: offers.end()); // by initial preference
318
319 int offset = -1;
320 KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(name: stName);
321 if (entry) {
322 offset = entry->offset();
323 // Q_ASSERT(str.device()->pos() == entry->serviceOffersOffset() + m_offerListOffset);
324 }
325 if (offset == -1) {
326 qCDebug(SYCOCA) << "Didn't find servicetype or MIME type" << stName;
327 continue;
328 }
329
330 for (const auto &offer : std::as_const(t&: offers)) {
331 // qCDebug(SYCOCA) << stName << ": writing offer" << offer.service()->desktopEntryName() << offset << offer.service()->offset() << "in sycoca at
332 // pos" << str.device()->pos();
333 Q_ASSERT(offer.service()->offset() != 0);
334
335 str << qint32(offset);
336 str << qint32(offer.service()->offset());
337 str << qint32(offer.preference());
338 str << qint32(offer.mimeTypeInheritanceLevel());
339 // update offerEntrySize in populateServiceTypes if you add/remove something here
340 }
341 }
342
343 str << qint32(0); // End of list marker (0)
344}
345
346void KBuildServiceFactory::addEntry(const KSycocaEntry::Ptr &newEntry)
347{
348 Q_ASSERT(newEntry);
349 if (m_dupeDict.contains(value: newEntry)) {
350 return;
351 }
352
353 const KService::Ptr service(static_cast<KService *>(newEntry.data()));
354 m_dupeDict.insert(value: newEntry);
355 KSycocaFactory::addEntry(newEntry);
356}
357

source code of kservice/src/sycoca/kbuildservicefactory.cpp