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