1 /**
2 * This module provides class for loading and validating icon theme caches.
3 *
4 * Icon theme cache may be stored in icon-theme.cache files located in icon theme directory along with index.theme file.
5 * These files are usually generated by $(LINK2 https://developer.gnome.org/gtk3/stable/gtk-update-icon-cache.html, gtk-update-icon-cache).
6 * Icon theme cache can be used for faster and cheeper lookup of icons since it contains information about which icons exist in which sub directories.
7 *
8 * Authors:
9 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
10 * Copyright:
11 * Roman Chistokhodov, 2016
12 * License:
13 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
14 * See_Also:
15 * $(LINK2 https://github.com/GNOME/gtk/blob/master/gtk/gtkiconcachevalidator.c, GTK icon cache validator source code)
16 * Note:
17 * I could not find any specification on icon theme cache, so I merely use gtk source code as reference to reimplement parsing of icon-theme.cache files.
18 */
19
20
21 module icontheme.cache;
22
23 package {
24 import std.algorithm;
25 import std.bitmanip;
26 import std.exception;
27 import std.file;
28 import std.mmfile;
29 import std.path;
30 import std.range;
31 import std.system;
32 import std.typecons;
33 import std.traits;
34
35 import std.datetime : SysTime;
36
37 static if( __VERSION__ < 2066 ) enum nogc = 1;
38 }
39
40 private @nogc @trusted void swapByteOrder(T)(ref T t) nothrow pure {
41
42 static if( __VERSION__ < 2067 ) { //swapEndian was not @nogc
43 ubyte[] bytes = (cast(ubyte*)&t)[0..T.sizeof];
44 for (size_t i=0; i<bytes.length/2; ++i) {
45 ubyte tmp = bytes[i];
46 bytes[i] = bytes[T.sizeof-1-i];
47 bytes[T.sizeof-1-i] = tmp;
48 }
49 } else {
50 t = swapEndian(t);
51 }
52 }
53
54 /**
55 * Error occured while parsing icon theme cache.
56 */
57 class IconThemeCacheException : Exception
58 {
59 this(string msg, string context = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
60 super(msg, file, line, next);
61 _context = context;
62 }
63
64 /**
65 * Context where error occured. Usually it's the name of value that could not be read or is invalid.
66 */
67 @nogc @safe string context() const nothrow {
68 return _context;
69 }
70 private:
71 string _context;
72 }
73
74 /**
75 * Class representation of icon-theme.cache file contained icon theme cache.
76 */
77 final class IconThemeCache
78 {
79 /**
80 * Read icon theme cache from memory mapped file and validate it.
81 * Throws:
82 * $(B FileException) if could mmap file.
83 * $(D IconThemeCacheException) if icon theme file is invalid.
84 */
85 @trusted this(string fileName) {
86 _mmapped = new MmFile(fileName);
87 this(_mmapped[], fileName, 0);
88 }
89
90 /**
91 * Read icon theme cache from data and validate it.
92 * Throws:
93 * $(D IconThemeCacheException) if icon theme file is invalid.
94 */
95 @safe this(immutable(void)[] data, string fileName) {
96 this(data, fileName, 0);
97 }
98
99 private @trusted this(const(void)[] data, string fileName, int /* To avoid ambiguity */) {
100 _data = data;
101 _fileName = fileName;
102
103 _header.majorVersion = readValue!ushort(0, "major version");
104 if (_header.majorVersion != 1) {
105 throw new IconThemeCacheException("Unsupported version or the file is not icon theme cache", "major version");
106 }
107
108 _header.minorVersion = readValue!ushort(2, "minor version");
109 if (_header.minorVersion != 0) {
110 throw new IconThemeCacheException("Unsupported version or the file is not icon theme cache", "minor version");
111 }
112
113 _header.hashOffset = readValue!uint(4, "hash offset");
114 _header.directoryListOffset = readValue!uint(8, "directory list offset");
115
116 _bucketCount = iconOffsets().length;
117 _directoryCount = directories().length;
118
119 //Validate other data
120 foreach(dir; directories()) {
121 //pass
122 }
123
124 foreach(info; iconInfos) {
125 foreach(im; imageInfos(info.imageListOffset)) {
126
127 }
128 }
129 }
130
131 /**
132 * Sub directories of icon theme listed in cache.
133 * Returns: Range of directory const(char)[] names listed in cache.
134 */
135 @trusted auto directories() const {
136 auto directoryCount = readValue!uint(_header.directoryListOffset, "directory count");
137
138 return iota(directoryCount)
139 .map!(i => _header.directoryListOffset + uint.sizeof + i*uint.sizeof)
140 .map!(offset => readValue!uint(offset, "directory offset"))
141 .map!(offset => readString(offset, "directory name"));
142 }
143
144 /**
145 * Test if icon is listed in cache.
146 */
147 @trusted bool containsIcon(const(char)[] iconName) const
148 {
149 IconInfo info;
150 return findIconInfo(info, iconName);
151 }
152
153 /**
154 * Test if icon is listed in cache and belongs to specified subdirectory.
155 */
156 @trusted bool containsIcon(const(char)[] iconName, const(char)[] directory) const {
157 auto index = iconDirectories(iconName).countUntil(directory);
158 return index != -1;
159 }
160
161 /**
162 * Find all sub directories the icon belongs to according to cache.
163 * Returns: Range of directory const(char)[] names the icon belongs to.
164 */
165 @trusted auto iconDirectories(const(char)[] iconName) const
166 {
167 IconInfo info;
168 auto dirs = directories();
169 bool found = findIconInfo(info, iconName);
170 return imageInfos(info.imageListOffset, found).map!(delegate(ImageInfo im) {
171 if (im.index < dirs.length) {
172 return dirs[im.index];
173 } else {
174 throw new IconThemeCacheException("Invalid directory index", "directory index");
175 }
176 });
177 }
178
179 /**
180 * Path of cache file.
181 */
182 @nogc @safe fileName() const nothrow {
183 return _fileName;
184 }
185
186 /**
187 * Test if icon theme file is outdated, i.e. modification time of cache file is older than modification time of icon theme directory.
188 * Throws:
189 * $(B FileException) on error accessing the file.
190 */
191 @trusted bool isOutdated() const {
192 return isOutdated(fileName());
193 }
194
195 /**
196 * Test if icon theme file is outdated, i.e. modification time of cache file is older than modification time of icon theme directory.
197 *
198 * This function is static and therefore can be used before actual reading and validating cache file.
199 * Throws:
200 * $(B FileException) on error accessing the file.
201 */
202 static @trusted bool isOutdated(string fileName)
203 {
204 if (fileName.empty) {
205 throw new FileException("File name is empty, can't check if the cache is outdated");
206 }
207
208 SysTime pathAccessTime, pathModificationTime;
209 SysTime fileAccessTime, fileModificationTime;
210
211 getTimes(fileName, fileAccessTime, fileModificationTime);
212 getTimes(fileName.dirName, pathAccessTime, pathModificationTime);
213
214 return fileModificationTime < pathModificationTime;
215 }
216
217 unittest
218 {
219 assertThrown!FileException(isOutdated(""));
220 }
221
222 /**
223 * All icon names listed in cache.
224 * Returns: Range of icon const(char)[] names listed in cache.
225 */
226 @trusted auto icons() const {
227 return iconInfos().map!(info => info.name);
228 }
229
230 private:
231 alias Tuple!(uint, "chainOffset", const(char)[], "name", uint, "imageListOffset") IconInfo;
232 alias Tuple!(ushort, "index", ushort, "flags", uint, "dataOffset") ImageInfo;
233
234 static struct IconThemeCacheHeader
235 {
236 ushort majorVersion;
237 ushort minorVersion;
238 uint hashOffset;
239 uint directoryListOffset;
240 }
241
242 @trusted auto iconInfos() const {
243 import std.typecons;
244
245 static struct IconInfos
246 {
247 this(const(IconThemeCache) cache)
248 {
249 _cache = rebindable(cache);
250 _iconInfos = _cache.bucketIconInfos();
251 _chainOffset = _iconInfos.front().chainOffset;
252 _fromChain = false;
253 }
254
255 bool empty()
256 {
257 return _iconInfos.empty;
258 }
259
260 auto front()
261 {
262 if (_fromChain) {
263 auto info = _cache.iconInfo(_chainOffset);
264 return info;
265 } else {
266 auto info = _iconInfos.front;
267 return info;
268 }
269 }
270
271 void popFront()
272 {
273 if (_fromChain) {
274 auto info = _cache.iconInfo(_chainOffset);
275 if (info.chainOffset != 0xffffffff) {
276 _chainOffset = info.chainOffset;
277 } else {
278 _iconInfos.popFront();
279 _fromChain = false;
280 }
281 } else {
282 auto info = _iconInfos.front;
283 if (info.chainOffset != 0xffffffff) {
284 _chainOffset = info.chainOffset;
285 _fromChain = true;
286 } else {
287 _iconInfos.popFront();
288 }
289 }
290 }
291
292 auto save() const {
293 return this;
294 }
295
296 uint _chainOffset;
297 bool _fromChain;
298 typeof(_cache.bucketIconInfos()) _iconInfos;
299 Rebindable!(const(IconThemeCache)) _cache;
300 }
301
302 return IconInfos(this);
303 }
304
305 @nogc @trusted static uint iconNameHash(const(char)[] iconName) pure nothrow
306 {
307 if (iconName.length == 0) {
308 return 0;
309 }
310
311 uint h = cast(uint)iconName[0];
312 if (h) {
313 for (size_t i = 1; i != iconName.length; i++) {
314 h = (h << 5) - h + cast(uint)iconName[i];
315 }
316 }
317 return h;
318 }
319
320 bool findIconInfo(out IconInfo info, const(char)[] iconName) const {
321 uint hash = iconNameHash(iconName) % _bucketCount;
322 uint chainOffset = readValue!uint(_header.hashOffset + uint.sizeof + uint.sizeof * hash, "chain offset");
323
324 while(chainOffset != 0xffffffff) {
325 auto curInfo = iconInfo(chainOffset);
326 if (curInfo.name == iconName) {
327 info = curInfo;
328 return true;
329 }
330 chainOffset = curInfo.chainOffset;
331 }
332 return false;
333 }
334
335 @trusted auto bucketIconInfos() const {
336 return iconOffsets().filter!(offset => offset != 0xffffffff).map!(offset => iconInfo(offset));
337 }
338
339 @trusted auto iconOffsets() const {
340 auto bucketCount = readValue!uint(_header.hashOffset, "bucket count");
341
342 return iota(bucketCount)
343 .map!(i => _header.hashOffset + uint.sizeof + i*uint.sizeof)
344 .map!(offset => readValue!uint(offset, "icon offset"));
345 }
346
347 @trusted auto iconInfo(size_t iconOffset) const {
348 return IconInfo(
349 readValue!uint(iconOffset, "icon chain offset"),
350 readString(readValue!uint(iconOffset + uint.sizeof, "icon name offset"), "icon name"),
351 readValue!uint(iconOffset + uint.sizeof*2, "image list offset"));
352 }
353
354 @trusted auto imageInfos(size_t imageListOffset, bool found = true) const {
355
356 uint imageCount = found ? readValue!uint(imageListOffset, "image count") : 0;
357 return iota(imageCount)
358 .map!(i => imageListOffset + uint.sizeof + i*(uint.sizeof + ushort.sizeof + ushort.sizeof))
359 .map!(offset => ImageInfo(
360 readValue!ushort(offset, "image index"),
361 readValue!ushort(offset + ushort.sizeof, "image flags"),
362 readValue!uint(offset + ushort.sizeof*2, "image data offset"))
363 );
364 }
365
366 @trusted T readValue(T)(size_t offset, string context = null) const if (isIntegral!T || isSomeChar!T)
367 {
368 if (_data.length >= offset + T.sizeof) {
369 T value = *(cast(const(T)*)_data[offset..(offset+T.sizeof)].ptr);
370 static if (endian == Endian.littleEndian) {
371 swapByteOrder(value);
372 }
373 return value;
374 } else {
375 throw new IconThemeCacheException("Value is out of bounds", context);
376 }
377 }
378
379 @trusted auto readString(size_t offset, string context = null) const {
380 if (offset > _data.length) {
381 throw new IconThemeCacheException("Beginning of string is out of bounds", context);
382 }
383
384 auto str = cast(const(char[]))_data[offset.._data.length];
385
386 size_t len = 0;
387 while (len<str.length && str[len] != '\0') {
388 ++len;
389 }
390 if (len == str.length) {
391 throw new IconThemeCacheException("String is not zero terminated", context);
392 }
393
394 return str[0..len];
395 }
396
397 IconThemeCacheHeader _header;
398 size_t _directoryCount;
399 size_t _bucketCount;
400
401 MmFile _mmapped;
402 string _fileName;
403 const(void)[] _data;
404 }
405
406 ///
407 version(iconthemeFileTest) unittest
408 {
409 string cachePath = "./test/Tango/icon-theme.cache";
410 assert(cachePath.exists);
411
412 const(IconThemeCache) cache = new IconThemeCache(cachePath);
413 assert(cache.fileName == cachePath);
414 assert(cache.containsIcon("folder"));
415 assert(cache.containsIcon("folder", "24x24/places"));
416 assert(cache.containsIcon("edit-copy", "32x32/actions"));
417 assert(cache.iconDirectories("text-x-generic").canFind("32x32/mimetypes"));
418 assert(cache.directories().canFind("32x32/devices"));
419
420 auto icons = cache.icons();
421 assert(icons.canFind("folder"));
422 assert(icons.canFind("text-x-generic"));
423
424 try {
425 SysTime pathAccessTime, pathModificationTime;
426 SysTime fileAccessTime, fileModificationTime;
427
428 getTimes(cachePath, fileAccessTime, fileModificationTime);
429 getTimes(cachePath.dirName, pathAccessTime, pathModificationTime);
430
431 setTimes(cachePath, pathAccessTime, pathModificationTime);
432 assert(!IconThemeCache.isOutdated(cachePath));
433 }
434 catch(Exception e) {
435 // some environmental error, just ignore
436 }
437
438 try {
439 auto fileData = assumeUnique(std.file.read(cachePath));
440 assertNotThrown(new IconThemeCache(fileData, cachePath));
441 } catch(FileException e) {
442
443 }
444
445 immutable(ubyte)[] data = [0,2,0,0];
446 IconThemeCacheException thrown = collectException!IconThemeCacheException(new IconThemeCache(data, cachePath));
447 assert(thrown !is null, "Invalid cache must throw");
448 assert(thrown.context == "major version");
449
450 data = [0,1,0,1];
451 thrown = collectException!IconThemeCacheException(new IconThemeCache(data, cachePath));
452 assert(thrown !is null, "Invalid cache must throw");
453 assert(thrown.context == "minor version");
454 }