1 /*
2  * SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
3  *
4  * SPDX-License-Identifier: LGPL-2.1-or-later
5  *
6  */
7 #include "icontheme.h"
8 #include <fcntl.h>
9 #include <sys/mman.h>
10 #include <sys/stat.h>
11 #include <ctime>
12 #include <fstream>
13 #include <limits>
14 #include <stdexcept>
15 #include <unordered_set>
16 #include "fcitx-config/iniparser.h"
17 #include "fcitx-config/marshallfunction.h"
18 #include "fcitx-utils/fs.h"
19 #include "fcitx-utils/log.h"
20 #include "fcitx-utils/mtime_p.h"
21 #include "config.h"
22 #include "misc_p.h"
23 
24 namespace fcitx {
25 
pathToRoot(const RawConfig & config)26 std::string pathToRoot(const RawConfig &config) {
27     std::string path;
28     const auto *pConfig = &config;
29     size_t length = 0;
30     while (pConfig) {
31         if (pConfig->parent() && length) {
32             length += 1; // For "/";
33         }
34         length += pConfig->name().size();
35         pConfig = pConfig->parent();
36     }
37 
38     pConfig = &config;
39     path.resize(length);
40     size_t currentLength = 0;
41     while (pConfig) {
42         if (pConfig->parent() && currentLength) {
43             currentLength += 1; // For "/";
44             path[length - currentLength] = '/';
45         }
46         const auto &seg = pConfig->name();
47         currentLength += seg.size();
48         path.replace(length - currentLength, seg.size(), seg);
49         pConfig = pConfig->parent();
50     }
51     return path;
52 }
53 
54 class IconThemeDirectoryPrivate {
55 public:
IconThemeDirectoryPrivate(const RawConfig & config)56     IconThemeDirectoryPrivate(const RawConfig &config)
57         : path_(pathToRoot(config)) {
58         if (path_.empty() || path_[0] == '/') {
59             throw std::invalid_argument("Invalid path.");
60         }
61 
62         if (auto subConfig = config.get("Size")) {
63             unmarshallOption(size_, *subConfig, false);
64         }
65         if (size_ <= 0) {
66             throw std::invalid_argument("Invalid size");
67         }
68 
69         if (auto subConfig = config.get("Scale")) {
70             unmarshallOption(size_, *subConfig, false);
71         }
72         if (auto subConfig = config.get("Context")) {
73             unmarshallOption(context_, *subConfig, false);
74         }
75         if (auto subConfig = config.get("Type")) {
76             unmarshallOption(type_, *subConfig, false);
77         }
78         if (auto subConfig = config.get("MaxSize")) {
79             unmarshallOption(maxSize_, *subConfig, false);
80         }
81         if (auto subConfig = config.get("MinSize")) {
82             unmarshallOption(minSize_, *subConfig, false);
83         }
84         if (auto subConfig = config.get("Threshold")) {
85             unmarshallOption(threshold_, *subConfig, false);
86         }
87 
88         if (maxSize_ <= 0) {
89             maxSize_ = size_;
90         }
91 
92         if (minSize_ <= 0) {
93             minSize_ = size_;
94         }
95     }
96 
97     FCITX_INLINE_DEFINE_DEFAULT_DTOR_COPY_AND_MOVE_WITHOUT_SPEC(
98         IconThemeDirectoryPrivate);
99 
100     std::string path_;
101     int size_ = 0;
102     int scale_ = 1;
103     std::string context_;
104     IconThemeDirectoryType type_ = IconThemeDirectoryType::Threshold;
105     int maxSize_ = 0;
106     int minSize_ = 0;
107     int threshold_ = 2;
108 };
109 
IconThemeDirectory(const RawConfig & config)110 IconThemeDirectory::IconThemeDirectory(const RawConfig &config)
111     : d_ptr(std::make_unique<IconThemeDirectoryPrivate>(config)) {}
112 
113 FCITX_DEFINE_DPTR_COPY_AND_DEFAULT_DTOR_AND_MOVE(IconThemeDirectory);
114 
matchesSize(int iconsize,int iconscale) const115 bool IconThemeDirectory::matchesSize(int iconsize, int iconscale) const {
116     if (scale() != iconscale) {
117         return false;
118     }
119     switch (type()) {
120     case IconThemeDirectoryType::Fixed:
121         return iconsize == size();
122     case IconThemeDirectoryType::Scalable:
123         return minSize() <= iconsize && iconsize <= maxSize();
124     case IconThemeDirectoryType::Threshold:
125         return size() - threshold() <= iconsize &&
126                iconsize <= size() + threshold();
127     }
128     return false;
129 }
130 
sizeDistance(int iconsize,int iconscale) const131 int IconThemeDirectory::sizeDistance(int iconsize, int iconscale) const {
132     switch (type()) {
133     case IconThemeDirectoryType::Fixed:
134         return std::abs(size() * scale() - iconsize * iconscale);
135     case IconThemeDirectoryType::Scalable:
136         if (iconsize * iconscale < minSize() * scale()) {
137             return minSize() * scale() - iconsize * iconscale;
138         }
139         if (iconsize * iconscale > maxSize() * scale()) {
140             return iconsize * iconscale - maxSize() * scale();
141         }
142         return 0;
143     case IconThemeDirectoryType::Threshold:
144         if (iconsize * iconscale < (size() - threshold()) * scale()) {
145             return (size() - threshold()) * scale() - iconsize * iconscale;
146         }
147         if (iconsize * iconscale > (size() + threshold()) * scale()) {
148             return iconsize * iconscale - (size() - threshold()) * scale();
149         }
150     }
151     return 0;
152 }
153 
154 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, std::string, path);
155 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, int, size);
156 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, int, scale);
157 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, std::string,
158                                         context);
159 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory,
160                                         IconThemeDirectoryType, type);
161 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, int, maxSize);
162 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, int, minSize);
163 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, int, threshold);
164 
timespecLess(const Timespec & lhs,const Timespec & rhs)165 bool timespecLess(const Timespec &lhs, const Timespec &rhs) {
166     if (lhs.sec != rhs.sec) {
167         return lhs.sec < rhs.sec;
168     }
169     return lhs.nsec < rhs.nsec;
170 }
171 
iconNameHash(const char * p)172 static uint32_t iconNameHash(const char *p) {
173     uint32_t h = static_cast<signed char>(*p);
174     for (p += 1; *p != '\0'; p++) {
175         h = (h << 5) - h + *p;
176     }
177     return h;
178 }
179 
180 class IconThemeCache {
181 public:
IconThemeCache(const std::string & filename)182     IconThemeCache(const std::string &filename) {
183         auto fd = UnixFD::own(open(filename.c_str(), O_RDONLY));
184         if (!fd.isValid()) {
185             return;
186         }
187         struct stat st;
188         if (fstat(fd.fd(), &st) != 0) {
189             return;
190         }
191         struct stat dirSt;
192         auto dirName = fs::dirName(filename);
193         if (stat(dirName.c_str(), &dirSt) != 0) {
194             return;
195         }
196         if (timespecLess(modifiedTime(st), modifiedTime(dirSt))) {
197             return;
198         }
199 
200         memory_ = static_cast<uint8_t *>(
201             mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd.fd(), 0));
202         if (!memory_) {
203             // Failed to mmap is not critical here.
204             return;
205         }
206         size_ = st.st_size;
207 
208         if (readWord(0) != 1 || readWord(2) != 0) { // version major minor
209             return;
210         }
211 
212         isValid_ = true;
213 
214         // Check that all the directories are older than the cache
215         uint32_t dirListOffset = readDoubleWord(8);
216         uint32_t dirListLen = readDoubleWord(dirListOffset);
217         // We assume it won't be so long just in case we hit a corruptted file.
218         if (dirListLen > 4096) {
219             isValid_ = false;
220             return;
221         }
222         for (uint32_t i = 0; i < dirListLen; ++i) {
223             uint32_t offset = readDoubleWord(dirListOffset + 4 + 4 * i);
224             if (!isValid_ || offset >= size_) {
225                 isValid_ = false;
226                 return;
227             }
228             struct stat subDirSt;
229             auto *dir = checkString(offset);
230             if (!dir || stat(stringutils::joinPath(dirName, dir).c_str(),
231                              &subDirSt) != 0) {
232                 isValid_ = false;
233                 return;
234             }
235             if (timespecLess(modifiedTime(st), modifiedTime(subDirSt))) {
236                 isValid_ = false;
237                 return;
238             }
239         }
240     }
241 
242     IconThemeCache() = default;
IconThemeCache(IconThemeCache && other)243     IconThemeCache(IconThemeCache &&other) noexcept
244         : isValid_(other.isValid_), memory_(other.memory_), size_(other.size_) {
245         other.isValid_ = false;
246         other.memory_ = nullptr;
247         other.size_ = 0;
248     }
249 
operator =(IconThemeCache other)250     IconThemeCache &operator=(IconThemeCache other) {
251         std::swap(other.isValid_, isValid_);
252         std::swap(other.size_, size_);
253         std::swap(other.memory_, memory_);
254         return *this;
255     }
256 
isValid() const257     bool isValid() const { return isValid_; }
258 
~IconThemeCache()259     ~IconThemeCache() {
260         if (memory_) {
261             munmap(memory_, size_);
262         }
263     }
264 
readWord(uint32_t offset) const265     uint16_t readWord(uint32_t offset) const {
266         if (offset > size_ - 2 || (offset % 2)) {
267             isValid_ = false;
268             return 0;
269         }
270         return memory_[offset + 1] | memory_[offset] << 8;
271     }
readDoubleWord(unsigned int offset) const272     uint32_t readDoubleWord(unsigned int offset) const {
273         if (offset > size_ - 4 || (offset % 4)) {
274             isValid_ = false;
275             return 0;
276         }
277         return memory_[offset + 3] | memory_[offset + 2] << 8 |
278                memory_[offset + 1] << 16 | memory_[offset] << 24;
279     }
280 
checkString(uint32_t offset) const281     char *checkString(uint32_t offset) const {
282         // assume string length is less than 1k.
283         for (uint32_t i = 0; i < 1024; i++) {
284             if (offset + i >= size_) {
285                 return nullptr;
286             }
287             auto c = memory_[offset + i];
288             if (c == '\0') {
289                 break;
290             }
291         }
292         return reinterpret_cast<char *>(memory_ + offset);
293     }
294 
295     std::unordered_set<std::string> lookup(const std::string &name) const;
296 
297     mutable bool isValid_ = false;
298     uint8_t *memory_ = nullptr;
299     size_t size_ = 0;
300 };
301 
302 std::unordered_set<std::string>
lookup(const std::string & name) const303 IconThemeCache::lookup(const std::string &name) const {
304     std::unordered_set<std::string> ret;
305     auto hash = iconNameHash(name.c_str());
306 
307     uint32_t hashOffset = readDoubleWord(4);
308     uint32_t hashBucketCount = readDoubleWord(hashOffset);
309 
310     if (!isValid_ || hashBucketCount == 0) {
311         isValid_ = false;
312         return ret;
313     }
314 
315     uint32_t bucketIndex = hash % hashBucketCount;
316     uint32_t bucketOffset = readDoubleWord(hashOffset + 4 + bucketIndex * 4);
317     while (bucketOffset > 0 && bucketOffset <= size_ - 12) {
318         uint32_t nameOff = readDoubleWord(bucketOffset + 4);
319         auto *namePtr = checkString(nameOff);
320         if (nameOff < size_ && namePtr && name == namePtr) {
321             uint32_t dirListOffset = readDoubleWord(8);
322             uint32_t dirListLen = readDoubleWord(dirListOffset);
323 
324             uint32_t listOffset = readDoubleWord(bucketOffset + 8);
325             uint32_t listLen = readDoubleWord(listOffset);
326 
327             if (!isValid_ || listOffset + 4 + 8 * listLen > size_) {
328                 isValid_ = false;
329                 return ret;
330             }
331 
332             ret.reserve(listLen);
333             for (uint32_t j = 0; j < listLen && isValid_; ++j) {
334                 uint32_t dirIndex = readWord(listOffset + 4 + 8 * j);
335                 uint32_t o = readDoubleWord(dirListOffset + 4 + dirIndex * 4);
336                 if (!isValid_ || dirIndex >= dirListLen || o >= size_) {
337                     isValid_ = false;
338                     return ret;
339                 }
340                 if (auto *str = checkString(o)) {
341                     ret.emplace(str);
342                 } else {
343                     return {};
344                 }
345             }
346             return ret;
347         }
348         bucketOffset = readDoubleWord(bucketOffset);
349     }
350     return ret;
351 }
352 
353 class IconThemePrivate : QPtrHolder<IconTheme> {
354 public:
IconThemePrivate(IconTheme * q,const StandardPath & path)355     IconThemePrivate(IconTheme *q, const StandardPath &path)
356         : QPtrHolder(q), standardPath_(path) {
357         const char *home = getenv("HOME");
358         if (home) {
359             home_ = home;
360         }
361     }
362 
parse(const RawConfig & config,IconTheme * parent)363     void parse(const RawConfig &config, IconTheme *parent) {
364         if (!parent) {
365             subThemeNames_.insert(internalName_);
366         }
367 
368         auto section = config.get("Icon Theme");
369         if (!section) {
370             // If it's top level theme, make it fallback to hicolor.
371             if (!parent) {
372                 addInherit("hicolor");
373             }
374             return;
375         }
376 
377         if (auto nameSection = section->get("Name")) {
378             unmarshallOption(name_, *nameSection, false);
379         }
380 
381         if (auto commentSection = section->get("Comment")) {
382             unmarshallOption(comment_, *commentSection, false);
383         }
384 
385         auto parseDirectory = [&config,
386                                section](const char *name,
387                                         std::vector<IconThemeDirectory> &dir) {
388             if (auto subConfig = section->get(name)) {
389                 std::string directories;
390                 unmarshallOption(directories, *subConfig, false);
391                 for (const auto &directory :
392                      stringutils::split(directories, ",")) {
393                     if (auto directoryConfig = config.get(directory)) {
394                         try {
395                             dir.emplace_back(*directoryConfig);
396                         } catch (...) {
397                         }
398                     }
399                 }
400             }
401         };
402         parseDirectory("Directories", directories_);
403         parseDirectory("ScaledDirectories", scaledDirectories_);
404         // if (auto subConfig = section->get("Hidden")) {
405         //    unmarshallOption(hidden_, *subConfig, false);
406         // }
407         if (auto subConfig = section->get("Example")) {
408             unmarshallOption(example_, *subConfig, false);
409         }
410         if (auto subConfig = section->get("Inherits")) {
411             std::string inherits;
412             unmarshallOption(inherits, *subConfig, false);
413             for (const auto &inherit : stringutils::split(inherits, ",")) {
414                 if (!parent) {
415                     addInherit(inherit);
416                 } else {
417                     parent->d_ptr->addInherit(inherit);
418                 }
419             }
420         }
421 
422         // Always inherit hicolor.
423         if (!parent && !subThemeNames_.count("hicolor")) {
424             addInherit("hicolor");
425         }
426     }
427 
addInherit(const std::string & inherit)428     void addInherit(const std::string &inherit) {
429         if (subThemeNames_.insert(inherit).second) {
430             try {
431                 inherits_.push_back(IconTheme(inherit, q_ptr, standardPath_));
432             } catch (...) {
433             }
434         }
435     }
436 
findIcon(const std::string & icon,int size,int scale,const std::vector<std::string> & extensions) const437     std::string findIcon(const std::string &icon, int size, int scale,
438                          const std::vector<std::string> &extensions) const {
439         // Respect absolute path.
440         if (icon.empty() || icon[0] == '/') {
441             return icon;
442         }
443 
444         std::string filename;
445         // If we're a empty theme, only look at fallback icon.
446         if (!internalName_.empty()) {
447             auto filename = findIconHelper(icon, size, scale, extensions);
448             if (!filename.empty()) {
449                 return filename;
450             }
451         }
452         filename = lookupFallbackIcon(icon, extensions);
453 
454         // We tries to violate the spec same as LXDE, only lookup dash fallback
455         // after looking through all existing themes.
456         if (filename.empty()) {
457             auto dashPos = icon.rfind('-');
458             if (dashPos != std::string::npos) {
459                 filename =
460                     findIcon(icon.substr(0, dashPos), size, scale, extensions);
461             }
462         }
463         return filename;
464     }
465 
466     std::string
findIconHelper(const std::string & icon,int size,int scale,const std::vector<std::string> & extensions) const467     findIconHelper(const std::string &icon, int size, int scale,
468                    const std::vector<std::string> &extensions) const {
469         auto filename = lookupIcon(icon, size, scale, extensions);
470         if (!filename.empty()) {
471             return filename;
472         }
473 
474         for (const auto &inherit : inherits_) {
475             filename =
476                 inherit.d_func()->lookupIcon(icon, size, scale, extensions);
477             if (!filename.empty()) {
478                 return filename;
479             }
480         }
481         return {};
482     }
483 
lookupIcon(const std::string & iconname,int size,int scale,const std::vector<std::string> & extensions) const484     std::string lookupIcon(const std::string &iconname, int size, int scale,
485                            const std::vector<std::string> &extensions) const {
486 
487         auto checkDirectory = [&extensions,
488                                &iconname](const IconThemeDirectory &directory,
489                                           std::string baseDir) -> std::string {
490             baseDir = stringutils::joinPath(baseDir, directory.path());
491             if (!fs::isdir(baseDir)) {
492                 return {};
493             }
494 
495             for (const auto &ext : extensions) {
496                 auto defaultPath = stringutils::joinPath(baseDir, iconname);
497                 defaultPath += ext;
498                 if (fs::isreg(defaultPath)) {
499                     return defaultPath;
500                 }
501             }
502             return {};
503         };
504 
505         for (const auto &baseDir : baseDirs_) {
506             bool hasCache = false;
507             std::unordered_set<std::string> dirFilter;
508             if (baseDir.second.isValid()) {
509                 dirFilter = baseDir.second.lookup(iconname);
510                 hasCache = true;
511             }
512             for (const auto &directory : directories_) {
513                 if ((hasCache && !dirFilter.count(directory.path())) ||
514                     !directory.matchesSize(size, scale)) {
515                     continue;
516                 }
517                 auto path = checkDirectory(directory, baseDir.first);
518                 if (!path.empty()) {
519                     return path;
520                 }
521             }
522 
523             if (scale != 1) {
524                 for (const auto &directory : scaledDirectories_) {
525                     if ((hasCache && !dirFilter.count(directory.path())) ||
526                         !directory.matchesSize(size, scale)) {
527                         continue;
528                     }
529                     auto path = checkDirectory(directory, baseDir.first);
530                     if (!path.empty()) {
531                         return path;
532                     }
533                 }
534             }
535         }
536 
537         auto minSize = std::numeric_limits<int>::max();
538         std::string closestFilename;
539 
540         for (const auto &baseDir : baseDirs_) {
541             bool hasCache = false;
542             std::unordered_set<std::string> dirFilter;
543             if (baseDir.second.isValid()) {
544                 dirFilter = baseDir.second.lookup(iconname);
545                 hasCache = true;
546             }
547 
548             auto checkDirectoryWithSize =
549                 [&checkDirectory, &closestFilename, &dirFilter, hasCache, size,
550                  scale, &minSize, &baseDir](const IconThemeDirectory &dir) {
551                     if (hasCache && !dirFilter.count(dir.path())) {
552                         return;
553                     }
554                     auto distance = dir.sizeDistance(size, scale);
555                     if (distance < minSize) {
556                         auto path = checkDirectory(dir, baseDir.first);
557                         if (!path.empty()) {
558                             closestFilename = path;
559                             minSize = distance;
560                         }
561                     }
562                 };
563 
564             for (const auto &directory : directories_) {
565                 checkDirectoryWithSize(directory);
566             }
567 
568             if (scale != 1) {
569                 for (const auto &directory : scaledDirectories_) {
570                     checkDirectoryWithSize(directory);
571                 }
572             }
573         }
574 
575         return closestFilename;
576     }
577 
578     std::string
lookupFallbackIcon(const std::string & iconname,const std::vector<std::string> & extensions) const579     lookupFallbackIcon(const std::string &iconname,
580                        const std::vector<std::string> &extensions) const {
581         auto defaultBasePath = stringutils::joinPath(home_, ".icons", iconname);
582         for (const auto &ext : extensions) {
583             auto path = defaultBasePath + ext;
584             if (fs::isreg(path)) {
585                 return path;
586             }
587             path = standardPath_.locate(
588                 StandardPath::Type::Data,
589                 stringutils::joinPath("icons", iconname + ext));
590             if (!path.empty()) {
591                 return path;
592             }
593         }
594         return {};
595     }
596 
addBaseDir(const std::string & path)597     void addBaseDir(const std::string &path) {
598         if (!fs::isdir(path)) {
599             return;
600         }
601         baseDirs_.emplace_back(std::piecewise_construct,
602                                std::forward_as_tuple(path),
603                                std::forward_as_tuple(stringutils::joinPath(
604                                    path, "icon-theme.cache")));
605     }
606 
prepare()607     void prepare() {
608         if (!home_.empty()) {
609             addBaseDir(stringutils::joinPath(home_, ".icons", internalName_));
610         }
611         if (auto userDir =
612                 standardPath_.userDirectory(StandardPath::Type::Data);
613             !userDir.empty()) {
614             addBaseDir(stringutils::joinPath(userDir, "icons", internalName_));
615         }
616 
617         for (auto &dataDir :
618              standardPath_.directories(StandardPath::Type::Data)) {
619             addBaseDir(stringutils::joinPath(dataDir, "icons", internalName_));
620         }
621     }
622 
623     std::string home_;
624     std::string internalName_;
625     const StandardPath &standardPath_;
626     I18NString name_;
627     I18NString comment_;
628     std::vector<IconTheme> inherits_;
629     std::vector<IconThemeDirectory> directories_;
630     std::vector<IconThemeDirectory> scaledDirectories_;
631     std::unordered_set<std::string> subThemeNames_;
632     std::vector<std::pair<std::string, IconThemeCache>> baseDirs_;
633     // Not really useful for our usecase.
634     // bool hidden_;
635     std::string example_;
636 };
637 
IconTheme(const std::string & name,const StandardPath & standardPath)638 IconTheme::IconTheme(const std::string &name, const StandardPath &standardPath)
639     : IconTheme(name, nullptr, standardPath) {}
640 
IconTheme(const std::string & name,IconTheme * parent,const StandardPath & standardPath)641 IconTheme::IconTheme(const std::string &name, IconTheme *parent,
642                      const StandardPath &standardPath)
643     : IconTheme(standardPath) {
644     FCITX_D();
645     auto files = standardPath.openAll(
646         StandardPath::Type::Data,
647         stringutils::joinPath("icons", name, "index.theme"), O_RDONLY);
648 
649     RawConfig config;
650     for (auto iter = files.rbegin(), end = files.rend(); iter != end; iter++) {
651         readFromIni(config, iter->fd());
652     }
653     auto path = stringutils::joinPath(d->home_, ".icons", name, "index.theme");
654     auto fd = UnixFD::own(open(path.c_str(), O_RDONLY));
655     if (fd.fd() >= 0) {
656         readFromIni(config, fd.fd());
657     }
658 
659     d->parse(config, parent);
660     d->internalName_ = name;
661     d->prepare();
662 }
663 
IconTheme(const StandardPath & standardPath)664 IconTheme::IconTheme(const StandardPath &standardPath)
665     : d_ptr(std::make_unique<IconThemePrivate>(this, standardPath)) {}
666 
667 FCITX_DEFINE_DEFAULT_DTOR_AND_MOVE(IconTheme);
668 
669 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconTheme, std::string, internalName);
670 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconTheme, I18NString, name);
671 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconTheme, I18NString, comment);
672 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconTheme, std::vector<IconTheme>,
673                                         inherits);
674 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconTheme,
675                                         std::vector<IconThemeDirectory>,
676                                         directories);
677 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconTheme,
678                                         std::vector<IconThemeDirectory>,
679                                         scaledDirectories);
680 FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconTheme, std::string, example);
681 
findIcon(const std::string & iconName,unsigned int desiredSize,int scale,const std::vector<std::string> & extensions)682 std::string IconTheme::findIcon(const std::string &iconName,
683                                 unsigned int desiredSize, int scale,
684                                 const std::vector<std::string> &extensions) {
685     return std::as_const(*this).findIcon(iconName, desiredSize, scale,
686                                          extensions);
687 }
688 
689 std::string
findIcon(const std::string & iconName,unsigned int desiredSize,int scale,const std::vector<std::string> & extensions) const690 IconTheme::findIcon(const std::string &iconName, unsigned int desiredSize,
691                     int scale,
692                     const std::vector<std::string> &extensions) const {
693     FCITX_D();
694     return d->findIcon(iconName, desiredSize, scale, extensions);
695 }
696 
getKdeTheme(int fd)697 std::string getKdeTheme(int fd) {
698     RawConfig rawConfig;
699     readFromIni(rawConfig, fd);
700     if (auto icons = rawConfig.get("Icons")) {
701         if (auto theme = icons->get("Theme")) {
702             if (!theme->value().empty() &&
703                 theme->value().find('/') == std::string::npos) {
704                 return theme->value();
705             }
706         }
707     }
708     return "";
709 }
710 
getGtkTheme(const std::string & filename)711 std::string getGtkTheme(const std::string &filename) {
712     // Evil Gtk2 use an non standard "ini-like" rc file.
713     // Grep it and try to find the line we want ourselves.
714     std::ifstream fin(filename, std::ios::in | std::ios::binary);
715     std::string line;
716     while (std::getline(fin, line)) {
717         auto tokens = stringutils::split(line, "=");
718         if (tokens.size() == 2 &&
719             stringutils::trim(tokens[0]) == "gtk-icon-theme-name") {
720             auto value = stringutils::trim(tokens[1]);
721             if (value.size() >= 2 && value.front() == '"' &&
722                 value.back() == '"') {
723                 value = value.substr(1, value.size() - 2);
724             }
725             if (!value.empty() && value.find('/') == std::string::npos) {
726                 return value;
727             }
728         }
729     }
730     return "";
731 }
732 
defaultIconThemeName()733 std::string IconTheme::defaultIconThemeName() {
734     DesktopType desktopType = getDesktopType();
735     switch (desktopType) {
736     case DesktopType::KDE5: {
737         auto files = StandardPath::global().openAll(StandardPath::Type::Config,
738                                                     "kdeglobals", O_RDONLY);
739         for (auto &file : files) {
740             auto theme = getKdeTheme(file.fd());
741             if (!theme.empty()) {
742                 return theme;
743             }
744         }
745 
746         return "breeze";
747     }
748     case DesktopType::KDE4: {
749         const char *home = getenv("HOME");
750         if (home && home[0]) {
751             std::string files[] = {
752                 stringutils::joinPath(home, ".kde4/share/config/kdeglobals"),
753                 stringutils::joinPath(home, ".kde/share/config/kdeglobals"),
754                 "/etc/kde4/kdeglobals"};
755             for (auto &file : files) {
756                 auto fd = UnixFD::own(open(file.c_str(), O_RDONLY));
757                 auto theme = getKdeTheme(fd.fd());
758                 if (!theme.empty()) {
759                     return theme;
760                 }
761             }
762         }
763         return "oxygen";
764     }
765     default: {
766         auto files = StandardPath::global().locateAll(
767             StandardPath::Type::Config, "gtk-3.0/settings.ini");
768         for (auto &file : files) {
769             auto theme = getGtkTheme(file);
770             if (!theme.empty()) {
771                 return theme;
772             }
773         }
774         auto theme = getGtkTheme("/etc/gtk-3.0/settings.ini");
775         if (!theme.empty()) {
776             return theme;
777         }
778         const char *home = getenv("HOME");
779         if (home && home[0]) {
780             std::string homeStr(home);
781             std::string files[] = {stringutils::joinPath(homeStr, ".gtkrc-2.0"),
782                                    "/etc/gtk-2.0/gtkrc"};
783             for (auto &file : files) {
784                 auto theme = getGtkTheme(file);
785                 if (!theme.empty()) {
786                     return theme;
787                 }
788             }
789         }
790     } break;
791     }
792 
793     if (desktopType == DesktopType::Unknown) {
794         return "Tango";
795     } else if (desktopType == DesktopType::GNOME) {
796         return "Adwaita";
797     }
798     return "gnome";
799 }
800 
801 /// Rename fcitx-* icon to org.fcitx.Fcitx5.fcitx-* if in flatpak
iconName(const std::string & icon,bool inFlatpak)802 std::string IconTheme::iconName(const std::string &icon, bool inFlatpak) {
803 #ifdef USE_FLATPAK_ICON
804     if (inFlatpak && stringutils::startsWith(icon, "fcitx-")) {
805         return stringutils::concat("org.fcitx.Fcitx5.", icon);
806     }
807 #else
808     FCITX_UNUSED(inFlatpak);
809 #endif
810     return icon;
811 }
812 } // namespace fcitx
813