/** * This module provides class for loading and validating icon theme caches. * * Icon theme cache may be stored in icon-theme.cache files located in icon theme directory along with index.theme file. * These files are usually generated by $(LINK2 https://developer.gnome.org/gtk3/stable/gtk-update-icon-cache.html, gtk-update-icon-cache). * 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. * * Authors: * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) * Copyright: * Roman Chistokhodov, 2016 * License: * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). * See_Also: * $(LINK2 https://github.com/GNOME/gtk/blob/master/gtk/gtkiconcachevalidator.c, GTK icon cache validator source code) * Note: * 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. */ module icontheme.cache; package { import std.algorithm; import std.bitmanip; import std.exception; import std.file; import std.mmfile; import std.path; import std.range; import std.system; import std.typecons; import std.traits; import std.datetime : SysTime; static if( __VERSION__ < 2066 ) enum nogc = 1; } private @nogc @trusted void swapByteOrder(T)(ref T t) nothrow pure { static if( __VERSION__ < 2067 ) { //swapEndian was not @nogc ubyte[] bytes = (cast(ubyte*)&t)[0..T.sizeof]; for (size_t i=0; i _header.directoryListOffset + uint.sizeof + i*uint.sizeof) .map!(offset => readValue!uint(offset, "directory offset")) .map!(offset => readString(offset, "directory name")); } /** * Test if icon is listed in cache. */ @trusted bool containsIcon(const(char)[] iconName) const { IconInfo info; return findIconInfo(info, iconName); } /** * Test if icon is listed in cache and belongs to specified subdirectory. */ @trusted bool containsIcon(const(char)[] iconName, const(char)[] directory) const { auto index = iconDirectories(iconName).countUntil(directory); return index != -1; } /** * Find all sub directories the icon belongs to according to cache. * Returns: Range of directory const(char)[] names the icon belongs to. */ @trusted auto iconDirectories(const(char)[] iconName) const { IconInfo info; auto dirs = directories(); bool found = findIconInfo(info, iconName); return imageInfos(info.imageListOffset, found).map!(delegate(ImageInfo im) { if (im.index < dirs.length) { return dirs[im.index]; } else { throw new IconThemeCacheException("Invalid directory index", "directory index"); } }); } /** * Path of cache file. */ @nogc @safe fileName() const nothrow { return _fileName; } /** * Test if icon theme file is outdated, i.e. modification time of cache file is older than modification time of icon theme directory. * Throws: * $(B FileException) on error accessing the file. */ @trusted bool isOutdated() const { return isOutdated(fileName()); } /** * Test if icon theme file is outdated, i.e. modification time of cache file is older than modification time of icon theme directory. * * This function is static and therefore can be used before actual reading and validating cache file. * Throws: * $(B FileException) on error accessing the file. */ static @trusted bool isOutdated(string fileName) { if (fileName.empty) { throw new FileException("File name is empty, can't check if the cache is outdated"); } SysTime pathAccessTime, pathModificationTime; SysTime fileAccessTime, fileModificationTime; getTimes(fileName, fileAccessTime, fileModificationTime); getTimes(fileName.dirName, pathAccessTime, pathModificationTime); return fileModificationTime < pathModificationTime; } unittest { assertThrown!FileException(isOutdated("")); } /** * All icon names listed in cache. * Returns: Range of icon const(char)[] names listed in cache. */ @trusted auto icons() const { return iconInfos().map!(info => info.name); } private: alias Tuple!(uint, "chainOffset", const(char)[], "name", uint, "imageListOffset") IconInfo; alias Tuple!(ushort, "index", ushort, "flags", uint, "dataOffset") ImageInfo; static struct IconThemeCacheHeader { ushort majorVersion; ushort minorVersion; uint hashOffset; uint directoryListOffset; } @trusted auto iconInfos() const { import std.typecons; static struct IconInfos { this(const(IconThemeCache) cache) { _cache = rebindable(cache); _iconInfos = _cache.bucketIconInfos(); _chainOffset = _iconInfos.front().chainOffset; _fromChain = false; } bool empty() { return _iconInfos.empty; } auto front() { if (_fromChain) { auto info = _cache.iconInfo(_chainOffset); return info; } else { auto info = _iconInfos.front; return info; } } void popFront() { if (_fromChain) { auto info = _cache.iconInfo(_chainOffset); if (info.chainOffset != 0xffffffff) { _chainOffset = info.chainOffset; } else { _iconInfos.popFront(); _fromChain = false; } } else { auto info = _iconInfos.front; if (info.chainOffset != 0xffffffff) { _chainOffset = info.chainOffset; _fromChain = true; } else { _iconInfos.popFront(); } } } auto save() const { return this; } uint _chainOffset; bool _fromChain; typeof(_cache.bucketIconInfos()) _iconInfos; Rebindable!(const(IconThemeCache)) _cache; } return IconInfos(this); } @nogc @trusted static uint iconNameHash(const(char)[] iconName) pure nothrow { if (iconName.length == 0) { return 0; } uint h = cast(uint)iconName[0]; if (h) { for (size_t i = 1; i != iconName.length; i++) { h = (h << 5) - h + cast(uint)iconName[i]; } } return h; } bool findIconInfo(out IconInfo info, const(char)[] iconName) const { uint hash = iconNameHash(iconName) % _bucketCount; uint chainOffset = readValue!uint(_header.hashOffset + uint.sizeof + uint.sizeof * hash, "chain offset"); while(chainOffset != 0xffffffff) { auto curInfo = iconInfo(chainOffset); if (curInfo.name == iconName) { info = curInfo; return true; } chainOffset = curInfo.chainOffset; } return false; } @trusted auto bucketIconInfos() const { return iconOffsets().filter!(offset => offset != 0xffffffff).map!(offset => iconInfo(offset)); } @trusted auto iconOffsets() const { auto bucketCount = readValue!uint(_header.hashOffset, "bucket count"); return iota(bucketCount) .map!(i => _header.hashOffset + uint.sizeof + i*uint.sizeof) .map!(offset => readValue!uint(offset, "icon offset")); } @trusted auto iconInfo(size_t iconOffset) const { return IconInfo( readValue!uint(iconOffset, "icon chain offset"), readString(readValue!uint(iconOffset + uint.sizeof, "icon name offset"), "icon name"), readValue!uint(iconOffset + uint.sizeof*2, "image list offset")); } @trusted auto imageInfos(size_t imageListOffset, bool found = true) const { uint imageCount = found ? readValue!uint(imageListOffset, "image count") : 0; return iota(imageCount) .map!(i => imageListOffset + uint.sizeof + i*(uint.sizeof + ushort.sizeof + ushort.sizeof)) .map!(offset => ImageInfo( readValue!ushort(offset, "image index"), readValue!ushort(offset + ushort.sizeof, "image flags"), readValue!uint(offset + ushort.sizeof*2, "image data offset")) ); } @trusted T readValue(T)(size_t offset, string context = null) const if (isIntegral!T || isSomeChar!T) { if (_data.length >= offset + T.sizeof) { T value = *(cast(const(T)*)_data[offset..(offset+T.sizeof)].ptr); static if (endian == Endian.littleEndian) { swapByteOrder(value); } return value; } else { throw new IconThemeCacheException("Value is out of bounds", context); } } @trusted auto readString(size_t offset, string context = null) const { if (offset > _data.length) { throw new IconThemeCacheException("Beginning of string is out of bounds", context); } auto str = cast(const(char[]))_data[offset.._data.length]; size_t len = 0; while (len