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

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