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 }