1 /*
2  * Copyright (c) 2015 Boudewijn Rempt <boud@valdyas.org>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17  */
18 #include "KoResourcePaths.h"
19 
20 #include <QGlobalStatic>
21 #include <QString>
22 #include <QStringList>
23 #include <QMap>
24 #include <QStandardPaths>
25 #include <QDir>
26 #include <QFileInfo>
27 #include <QDebug>
28 #include <QApplication>
29 #include <QMutex>
30 #include "kis_debug.h"
31 
32 
Q_GLOBAL_STATIC(KoResourcePaths,s_instance)33 Q_GLOBAL_STATIC(KoResourcePaths, s_instance)
34 
35 static QString cleanup(const QString &path)
36 {
37     return QDir::cleanPath(path);
38 }
39 
40 
cleanup(const QStringList & pathList)41 static QStringList cleanup(const QStringList &pathList)
42 {
43     QStringList cleanedPathList;
44     Q_FOREACH(const QString &path, pathList) {
45         cleanedPathList << cleanup(path);
46     }
47     return cleanedPathList;
48 }
49 
50 
cleanupDirs(const QString & path)51 static QString cleanupDirs(const QString &path)
52 {
53     return QDir::cleanPath(path) + '/';
54 }
55 
cleanupDirs(const QStringList & pathList)56 static QStringList cleanupDirs(const QStringList &pathList)
57 {
58     QStringList cleanedPathList;
59     Q_FOREACH(const QString &path, pathList) {
60         cleanedPathList << cleanupDirs(path);
61     }
62     return cleanedPathList;
63 }
64 
appendResources(QStringList * dst,const QStringList & src,bool eliminateDuplicates)65 void appendResources(QStringList *dst, const QStringList &src, bool eliminateDuplicates)
66 {
67     Q_FOREACH (const QString &resource, src) {
68         QString realPath = QDir::cleanPath(resource);
69         if (!eliminateDuplicates || !dst->contains(realPath)) {
70             *dst << realPath;
71         }
72     }
73 }
74 
75 
76 #ifdef Q_OS_WIN
77 static const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
78 #else
79 static const Qt::CaseSensitivity cs = Qt::CaseSensitive;
80 #endif
81 
82 #ifdef Q_OS_MACOS
83 #include <ApplicationServices/ApplicationServices.h>
84 #include <CoreFoundation/CoreFoundation.h>
85 #include <CoreServices/CoreServices.h>
86 #endif
87 
getInstallationPrefix()88 QString getInstallationPrefix() {
89 #ifdef Q_OS_MACOS
90     QString appPath = qApp->applicationDirPath();
91 
92     dbgResources << "1" << appPath;
93     appPath.chop(QString("MacOS/").length());
94     dbgResources << "2" << appPath;
95 
96     bool makeInstall = QDir(appPath + "/../../../share/kritaplugins").exists();
97     bool inBundle = QDir(appPath + "/Resources/kritaplugins").exists();
98 
99     dbgResources << "3. After make install" << makeInstall;
100     dbgResources << "4. In Bundle" << inBundle;
101 
102     QString bundlePath;
103 
104     if (inBundle) {
105         bundlePath = appPath + "/";
106     }
107     else if (makeInstall) {
108         appPath.chop(QString("Contents/").length());
109         bundlePath = appPath + "/../../";
110     }
111     else {
112         qFatal("Cannot calculate the bundle path from the app path");
113     }
114 
115     dbgResources << ">>>>>>>>>>>" << bundlePath;
116     return bundlePath;
117 #else
118 #ifdef Q_OS_QWIN
119     QDir appdir(qApp->applicationDirPath());
120 
121     // Corrects for mismatched case errors in path (qtdeclarative fails to load)
122     wchar_t buffer[1024];
123     QString absolute = appdir.absolutePath();
124     DWORD rv = ::GetShortPathName((wchar_t*)absolute.utf16(), buffer, 1024);
125     rv = ::GetLongPathName(buffer, buffer, 1024);
126     QString correctedPath((QChar *)buffer);
127     appdir.setPath(correctedPath);
128     appdir.cdUp();
129     return appdir.canonicalPath();
130 #else
131 #ifdef Q_OS_ANDROID
132     // qApp->applicationDirPath() isn't writable and android system won't allow
133     // any files other than libraries
134     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/";
135 #else
136     return qApp->applicationDirPath() + "/../";
137 #endif
138 #endif
139 #endif
140 }
141 
142 class Q_DECL_HIDDEN KoResourcePaths::Private {
143 public:
144     QMap<QString, QStringList> absolutes; // For each resource type, the list of absolute paths, from most local (most priority) to most global
145     QMap<QString, QStringList> relatives; // Same with relative paths
146 
147     QMutex relativesMutex;
148     QMutex absolutesMutex;
149 
aliases(const QString & type)150     QStringList aliases(const QString &type)
151     {
152         QStringList r;
153         QStringList a;
154         relativesMutex.lock();
155         if (relatives.contains(type)) {
156             r += relatives[type];
157         }
158         relativesMutex.unlock();
159         dbgResources << "\trelatives" << r;
160         absolutesMutex.lock();
161         if (absolutes.contains(type)) {
162             a += absolutes[type];
163         }
164         dbgResources << "\tabsolutes" << a;
165         absolutesMutex.unlock();
166 
167         return r + a;
168     }
169 
mapTypeToQStandardPaths(const QString & type)170     QStandardPaths::StandardLocation mapTypeToQStandardPaths(const QString &type)
171     {
172         if (type == "tmp") {
173             return QStandardPaths::TempLocation;
174         }
175         else if (type == "appdata") {
176             return QStandardPaths::AppDataLocation;
177         }
178         else if (type == "data") {
179             return QStandardPaths::AppDataLocation;
180         }
181         else if (type == "cache") {
182             return QStandardPaths::CacheLocation;
183         }
184         else if (type == "locale") {
185             return QStandardPaths::AppDataLocation;
186         }
187         else if (type == "genericdata") {
188             return QStandardPaths::GenericDataLocation;
189         }
190         else {
191             return QStandardPaths::AppDataLocation;
192         }
193     }
194 };
195 
KoResourcePaths()196 KoResourcePaths::KoResourcePaths()
197     : d(new Private)
198 {
199 }
200 
~KoResourcePaths()201 KoResourcePaths::~KoResourcePaths()
202 {
203 }
204 
getApplicationRoot()205 QString KoResourcePaths::getApplicationRoot()
206 {
207     return getInstallationPrefix();
208 }
209 
addResourceType(const char * type,const char * basetype,const QString & relativeName,bool priority)210 void KoResourcePaths::addResourceType(const char *type, const char *basetype,
211                                       const QString &relativeName, bool priority)
212 {
213     s_instance->addResourceTypeInternal(QString::fromLatin1(type), QString::fromLatin1(basetype), relativeName, priority);
214 }
215 
addResourceDir(const char * type,const QString & dir,bool priority)216 void KoResourcePaths::addResourceDir(const char *type, const QString &dir, bool priority)
217 {
218     s_instance->addResourceDirInternal(QString::fromLatin1(type), dir, priority);
219 }
220 
findResource(const char * type,const QString & fileName)221 QString KoResourcePaths::findResource(const char *type, const QString &fileName)
222 {
223     return cleanup(s_instance->findResourceInternal(QString::fromLatin1(type), fileName));
224 }
225 
findDirs(const char * type)226 QStringList KoResourcePaths::findDirs(const char *type)
227 {
228     return cleanupDirs(s_instance->findDirsInternal(QString::fromLatin1(type)));
229 }
230 
findAllResources(const char * type,const QString & filter,SearchOptions options)231 QStringList KoResourcePaths::findAllResources(const char *type,
232                                               const QString &filter,
233                                               SearchOptions options)
234 {
235     return cleanup(s_instance->findAllResourcesInternal(QString::fromLatin1(type), filter, options));
236 }
237 
resourceDirs(const char * type)238 QStringList KoResourcePaths::resourceDirs(const char *type)
239 {
240     return cleanupDirs(s_instance->resourceDirsInternal(QString::fromLatin1(type)));
241 }
242 
saveLocation(const char * type,const QString & suffix,bool create)243 QString KoResourcePaths::saveLocation(const char *type, const QString &suffix, bool create)
244 {
245     return cleanupDirs(s_instance->saveLocationInternal(QString::fromLatin1(type), suffix, create));
246 }
247 
locate(const char * type,const QString & filename)248 QString KoResourcePaths::locate(const char *type, const QString &filename)
249 {
250     return cleanup(s_instance->locateInternal(QString::fromLatin1(type), filename));
251 }
252 
locateLocal(const char * type,const QString & filename,bool createDir)253 QString KoResourcePaths::locateLocal(const char *type, const QString &filename, bool createDir)
254 {
255     return cleanup(s_instance->locateLocalInternal(QString::fromLatin1(type), filename, createDir));
256 }
257 
addResourceTypeInternal(const QString & type,const QString & basetype,const QString & relativename,bool priority)258 void KoResourcePaths::addResourceTypeInternal(const QString &type, const QString &basetype,
259                                               const QString &relativename,
260                                               bool priority)
261 {
262     Q_UNUSED(basetype);
263     if (relativename.isEmpty()) return;
264 
265     QString copy = relativename;
266 
267     Q_ASSERT(basetype == "data");
268 
269     if (!copy.endsWith(QLatin1Char('/'))) {
270         copy += QLatin1Char('/');
271     }
272 
273     d->relativesMutex.lock();
274     QStringList &rels = d->relatives[type]; // find or insert
275 
276     if (!rels.contains(copy, cs)) {
277         if (priority) {
278             rels.prepend(copy);
279         } else {
280             rels.append(copy);
281         }
282     }
283     d->relativesMutex.unlock();
284 
285     dbgResources << "addResourceType: type" << type << "basetype" << basetype << "relativename" << relativename << "priority" << priority << d->relatives[type];
286 }
287 
addResourceDirInternal(const QString & type,const QString & absdir,bool priority)288 void KoResourcePaths::addResourceDirInternal(const QString &type, const QString &absdir, bool priority)
289 {
290     if (absdir.isEmpty() || type.isEmpty()) return;
291 
292     // find or insert entry in the map
293     QString copy = absdir;
294     if (copy.at(copy.length() - 1) != QLatin1Char('/')) {
295         copy += QLatin1Char('/');
296     }
297 
298     d->absolutesMutex.lock();
299     QStringList &paths = d->absolutes[type];
300     if (!paths.contains(copy, cs)) {
301         if (priority) {
302             paths.prepend(copy);
303         } else {
304             paths.append(copy);
305         }
306     }
307     d->absolutesMutex.unlock();
308 
309     dbgResources << "addResourceDir: type" << type << "absdir" << absdir << "priority" << priority << d->absolutes[type];
310 }
311 
findResourceInternal(const QString & type,const QString & fileName)312 QString KoResourcePaths::findResourceInternal(const QString &type, const QString &fileName)
313 {
314     QStringList aliases = d->aliases(type);
315     dbgResources<< "aliases" << aliases << getApplicationRoot();
316     QString resource = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName, QStandardPaths::LocateFile);
317 
318     if (resource.isEmpty()) {
319         Q_FOREACH (const QString &alias, aliases) {
320             resource = QStandardPaths::locate(d->mapTypeToQStandardPaths(type), alias + '/' + fileName, QStandardPaths::LocateFile);
321             dbgResources << "\t1" << resource;
322             if (QFile::exists(resource)) {
323                 continue;
324             }
325         }
326     }
327     if (resource.isEmpty() || !QFile::exists(resource)) {
328         QString approot = getApplicationRoot();
329         Q_FOREACH (const QString &alias, aliases) {
330             resource = approot + "/share/" + alias + '/' + fileName;
331             dbgResources << "\t2" << resource;
332 
333             if (QFile::exists(resource)) {
334                 continue;
335             }
336         }
337     }
338     if (resource.isEmpty() || !QFile::exists(resource)) {
339         QString approot = getApplicationRoot();
340         Q_FOREACH (const QString &alias, aliases) {
341             resource = approot + "/share/krita/" + alias + '/' + fileName;
342             dbgResources << "\t3" << resource;
343             if (QFile::exists(resource)) {
344                 continue;
345             }
346         }
347     }
348 
349     if (resource.isEmpty() || !QFile::exists(resource)) {
350         QString extraResourceDirs = qgetenv("EXTRA_RESOURCE_DIRS");
351         if (!extraResourceDirs.isEmpty()) {
352             Q_FOREACH(const QString &extraResourceDir, extraResourceDirs.split(':', QString::SkipEmptyParts)) {
353                 if (aliases.isEmpty()) {
354                     resource = extraResourceDir + '/' + fileName;
355                     dbgResources<< "\t4" << resource;
356                     if (QFile::exists(resource)) {
357                         continue;
358                     }
359                 }
360                 else {
361                     Q_FOREACH (const QString &alias, aliases) {
362                         resource = extraResourceDir + '/' + alias + '/' + fileName;
363                         dbgResources<< "\t4" << resource;
364                         if (QFile::exists(resource)) {
365                             continue;
366                         }
367                     }
368                 }
369             }
370         }
371     }
372 
373     dbgResources<< "findResource: type" << type << "filename" << fileName << "resource" << resource;
374     Q_ASSERT(!resource.isEmpty());
375     return resource;
376 }
377 
378 
filesInDir(const QString & startdir,const QString & filter,bool recursive)379 QStringList filesInDir(const QString &startdir, const QString & filter, bool recursive)
380 {
381     dbgResources << "filesInDir: startdir" << startdir << "filter" << filter << "recursive" << recursive;
382     QStringList result;
383 
384     // First the entries in this path
385     QStringList nameFilters;
386     nameFilters << filter;
387     const QStringList fileNames = QDir(startdir).entryList(nameFilters, QDir::Files | QDir::CaseSensitive, QDir::Name);
388     dbgResources << "\tFound:" << fileNames.size() << ":" << fileNames;
389     Q_FOREACH (const QString &fileName, fileNames) {
390         QString file = startdir + '/' + fileName;
391         result << file;
392     }
393 
394     // And then everything underneath, if recursive is specified
395     if (recursive) {
396         const QStringList entries = QDir(startdir).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
397         Q_FOREACH (const QString &subdir, entries) {
398             dbgResources << "\tGoing to look in subdir" << subdir << "of" << startdir;
399             result << filesInDir(startdir + '/' + subdir, filter, recursive);
400         }
401     }
402     return result;
403 }
404 
findDirsInternal(const QString & type)405 QStringList KoResourcePaths::findDirsInternal(const QString &type)
406 {
407     QStringList aliases = d->aliases(type);
408     dbgResources << type << aliases << d->mapTypeToQStandardPaths(type);
409 
410     QStringList dirs;
411     QStringList standardDirs =
412             QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), "", QStandardPaths::LocateDirectory);
413     appendResources(&dirs, standardDirs, true);
414 
415     Q_FOREACH (const QString &alias, aliases) {
416         QStringList aliasDirs =
417                 QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias + '/', QStandardPaths::LocateDirectory);
418         appendResources(&dirs, aliasDirs, true);
419 
420 #ifdef Q_OS_MACOS
421         dbgResources << "MAC:" << getApplicationRoot();
422         QStringList bundlePaths;
423         bundlePaths << getApplicationRoot() + "/share/krita/" + alias;
424         bundlePaths << getApplicationRoot() + "/../share/krita/" + alias;
425         dbgResources << "bundlePaths" << bundlePaths;
426         appendResources(&dirs, bundlePaths, true);
427         Q_ASSERT(!dirs.isEmpty());
428 #endif
429 
430         QStringList fallbackPaths;
431         fallbackPaths << getApplicationRoot() + "/share/" + alias;
432         fallbackPaths << getApplicationRoot() + "/share/krita/" + alias;
433         appendResources(&dirs, fallbackPaths, true);
434 
435     }
436     dbgResources << "findDirs: type" << type << "resource" << dirs;
437     return dirs;
438 }
439 
440 
findAllResourcesInternal(const QString & type,const QString & _filter,SearchOptions options) const441 QStringList KoResourcePaths::findAllResourcesInternal(const QString &type,
442                                                       const QString &_filter,
443                                                       SearchOptions options) const
444 {
445     dbgResources << "=====================================================";
446     dbgResources << type << _filter << QStandardPaths::standardLocations(d->mapTypeToQStandardPaths(type));
447 
448     bool recursive = options & KoResourcePaths::Recursive;
449 
450     dbgResources << "findAllResources: type" << type << "filter" << _filter << "recursive" << recursive;
451 
452     QStringList aliases = d->aliases(type);
453     QString filter = _filter;
454 
455     // In cases where the filter  is like "color-schemes/*.colors" instead of "*.kpp", used with unregistered resource types
456     if (filter.indexOf('*') > 0) {
457         aliases << filter.split('*').first();
458         filter = '*' + filter.split('*')[1];
459         dbgResources << "Split up alias" << aliases << "filter" << filter;
460     }
461 
462     QStringList resources;
463     if (aliases.isEmpty()) {
464         QStringList standardResources =
465                 QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type),
466                                           filter, QStandardPaths::LocateFile);
467         dbgResources << "standardResources" << standardResources;
468         appendResources(&resources, standardResources, true);
469         dbgResources << "1" << resources;
470     }
471 
472 
473     QString extraResourceDirs = qgetenv("EXTRA_RESOURCE_DIRS");
474     dbgResources << "extraResourceDirs" << extraResourceDirs;
475     if (!extraResourceDirs.isEmpty()) {
476         Q_FOREACH(const QString &extraResourceDir, extraResourceDirs.split(':', QString::SkipEmptyParts)) {
477             if (aliases.isEmpty()) {
478                 appendResources(&resources, filesInDir(extraResourceDir + '/' + type, filter, recursive), true);
479             }
480             else {
481                 Q_FOREACH (const QString &alias, aliases) {
482                     appendResources(&resources, filesInDir(extraResourceDir + '/' + alias + '/', filter, recursive), true);
483                 }
484             }
485         }
486 
487     }
488 
489     dbgResources << "\tresources from qstandardpaths:" << resources.size();
490 
491     Q_FOREACH (const QString &alias, aliases) {
492         dbgResources << "\t\talias:" << alias;
493         QStringList dirs;
494 
495         QFileInfo dirInfo(alias);
496         if (dirInfo.exists() && dirInfo.isDir() && dirInfo.isAbsolute()) {
497             dirs << alias;
498         } else {
499             dirs << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory)
500                  << getInstallationPrefix() + "share/" + alias + "/"
501                  << getInstallationPrefix() + "share/krita/" + alias + "/";
502         }
503 
504         Q_FOREACH (const QString &dir, dirs) {
505             appendResources(&resources,
506                             filesInDir(dir, filter, recursive),
507                             true);
508         }
509     }
510 
511     dbgResources << "\tresources also from aliases:" << resources.size();
512 
513     // if the original filter is "input/*", we only want share/input/* and share/krita/input/* here, but not
514     // share/*. therefore, use _filter here instead of filter which was split into alias and "*".
515     QFileInfo fi(_filter);
516 
517     QStringList prefixResources;
518     prefixResources << filesInDir(getInstallationPrefix() + "share/" + fi.path(), fi.fileName(), false);
519     prefixResources << filesInDir(getInstallationPrefix() + "share/krita/" + fi.path(), fi.fileName(), false);
520     appendResources(&resources, prefixResources, true);
521 
522     dbgResources << "\tresources from installation:" << resources.size();
523     dbgResources << "=====================================================";
524 
525     return resources;
526 }
527 
resourceDirsInternal(const QString & type)528 QStringList KoResourcePaths::resourceDirsInternal(const QString &type)
529 {
530     QStringList resourceDirs;
531     QStringList aliases = d->aliases(type);
532 
533     Q_FOREACH (const QString &alias, aliases) {
534         QStringList aliasDirs;
535 
536         aliasDirs << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory);
537 
538         aliasDirs << getInstallationPrefix() + "share/" + alias + "/"
539                   << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory);
540         aliasDirs << getInstallationPrefix() + "share/krita/" + alias + "/"
541                   << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory);
542 
543         appendResources(&resourceDirs, aliasDirs, true);
544     }
545 
546     dbgResources << "resourceDirs: type" << type << resourceDirs;
547 
548     return resourceDirs;
549 }
550 
saveLocationInternal(const QString & type,const QString & suffix,bool create)551 QString KoResourcePaths::saveLocationInternal(const QString &type, const QString &suffix, bool create)
552 {
553     QStringList aliases = d->aliases(type);
554     QString path;
555     if (aliases.size() > 0) {
556         path = QStandardPaths::writableLocation(d->mapTypeToQStandardPaths(type)) + '/' + aliases.first();
557     }
558     else {
559         path = QStandardPaths::writableLocation(d->mapTypeToQStandardPaths(type));
560 
561 #ifndef Q_OS_ANDROID
562         // on Android almost all config locations we save to are app specific,
563         // and don't end with "krita".
564         if (!path.endsWith("krita")) {
565             path += "/krita";
566         }
567 #endif
568 
569         if (!suffix.isEmpty()) {
570             path += "/" + suffix;
571         }
572     }
573 
574     QDir d(path);
575 
576     if (!d.exists() && create) {
577         d.mkpath(path);
578     }
579     dbgResources << "saveLocation: type" << type << "suffix" << suffix << "create" << create << "path" << path;
580 
581     return path;
582 }
583 
locateInternal(const QString & type,const QString & filename)584 QString KoResourcePaths::locateInternal(const QString &type, const QString &filename)
585 {
586     QStringList aliases = d->aliases(type);
587 
588     QStringList locations;
589     if (aliases.isEmpty()) {
590         locations << QStandardPaths::locate(d->mapTypeToQStandardPaths(type), filename, QStandardPaths::LocateFile);
591     }
592 
593     Q_FOREACH (const QString &alias, aliases) {
594         locations << QStandardPaths::locate(d->mapTypeToQStandardPaths(type),
595                                             (alias.endsWith('/') ? alias : alias + '/') + filename, QStandardPaths::LocateFile);
596     }
597     dbgResources << "locate: type" << type << "filename" << filename << "locations" << locations;
598     if (locations.size() > 0) {
599         return locations.first();
600     }
601     else {
602         return "";
603     }
604 }
605 
locateLocalInternal(const QString & type,const QString & filename,bool createDir)606 QString KoResourcePaths::locateLocalInternal(const QString &type, const QString &filename, bool createDir)
607 {
608     QString path = saveLocationInternal(type, "", createDir);
609     dbgResources << "locateLocal: type" << type << "filename" << filename << "CreateDir" << createDir << "path" << path;
610     return path + '/' + filename;
611 }
612