dlangui/3rdparty/icontheme/cache.d

455 lines
15 KiB
D

/**
* 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<bytes.length/2; ++i) {
ubyte tmp = bytes[i];
bytes[i] = bytes[T.sizeof-1-i];
bytes[T.sizeof-1-i] = tmp;
}
} else {
t = swapEndian(t);
}
}
/**
* Error occured while parsing icon theme cache.
*/
class IconThemeCacheException : Exception
{
this(string msg, string context = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
super(msg, file, line, next);
_context = context;
}
/**
* Context where error occured. Usually it's the name of value that could not be read or is invalid.
*/
@nogc @safe string context() const nothrow {
return _context;
}
private:
string _context;
}
/**
* Class representation of icon-theme.cache file contained icon theme cache.
*/
final class IconThemeCache
{
/**
* Read icon theme cache from memory mapped file and validate it.
* Throws:
* $(B FileException) if could mmap file.
* $(D IconThemeCacheException) if icon theme file is invalid.
*/
@trusted this(string fileName) {
_mmapped = new MmFile(fileName);
this(_mmapped[], fileName, 0);
}
/**
* Read icon theme cache from data and validate it.
* Throws:
* $(D IconThemeCacheException) if icon theme file is invalid.
*/
@safe this(immutable(void)[] data, string fileName) {
this(data, fileName, 0);
}
private @trusted this(const(void)[] data, string fileName, int /* To avoid ambiguity */) {
_data = data;
_fileName = fileName;
_header.majorVersion = readValue!ushort(0, "major version");
if (_header.majorVersion != 1) {
throw new IconThemeCacheException("Unsupported version or the file is not icon theme cache", "major version");
}
_header.minorVersion = readValue!ushort(2, "minor version");
if (_header.minorVersion != 0) {
throw new IconThemeCacheException("Unsupported version or the file is not icon theme cache", "minor version");
}
_header.hashOffset = readValue!uint(4, "hash offset");
_header.directoryListOffset = readValue!uint(8, "directory list offset");
_bucketCount = iconOffsets().length;
_directoryCount = directories().length;
//Validate other data
foreach(dir; directories()) {
//pass
}
foreach(info; iconInfos) {
foreach(im; imageInfos(info.imageListOffset)) {
}
}
}
/**
* Sub directories of icon theme listed in cache.
* Returns: Range of directory const(char)[] names listed in cache.
*/
@trusted auto directories() const {
auto directoryCount = readValue!uint(_header.directoryListOffset, "directory count");
return iota(directoryCount)
.map!(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<str.length && str[len] != '\0') {
++len;
}
if (len == str.length) {
throw new IconThemeCacheException("String is not zero terminated", context);
}
return str[0..len];
}
IconThemeCacheHeader _header;
size_t _directoryCount;
size_t _bucketCount;
MmFile _mmapped;
string _fileName;
const(void)[] _data;
}
///
version(iconthemeFileTest) unittest
{
string cachePath = "./test/Tango/icon-theme.cache";
assert(cachePath.exists);
const(IconThemeCache) cache = new IconThemeCache(cachePath);
assert(cache.fileName == cachePath);
assert(cache.containsIcon("folder"));
assert(cache.containsIcon("folder", "24x24/places"));
assert(cache.containsIcon("edit-copy", "32x32/actions"));
assert(cache.iconDirectories("text-x-generic").canFind("32x32/mimetypes"));
assert(cache.directories().canFind("32x32/devices"));
auto icons = cache.icons();
assert(icons.canFind("folder"));
assert(icons.canFind("text-x-generic"));
try {
SysTime pathAccessTime, pathModificationTime;
SysTime fileAccessTime, fileModificationTime;
getTimes(cachePath, fileAccessTime, fileModificationTime);
getTimes(cachePath.dirName, pathAccessTime, pathModificationTime);
setTimes(cachePath, pathAccessTime, pathModificationTime);
assert(!IconThemeCache.isOutdated(cachePath));
}
catch(Exception e) {
// some environmental error, just ignore
}
try {
auto fileData = assumeUnique(std.file.read(cachePath));
assertNotThrown(new IconThemeCache(fileData, cachePath));
} catch(FileException e) {
}
immutable(ubyte)[] data = [0,2,0,0];
IconThemeCacheException thrown = collectException!IconThemeCacheException(new IconThemeCache(data, cachePath));
assert(thrown !is null, "Invalid cache must throw");
assert(thrown.context == "major version");
data = [0,1,0,1];
thrown = collectException!IconThemeCacheException(new IconThemeCache(data, cachePath));
assert(thrown !is null, "Invalid cache must throw");
assert(thrown.context == "minor version");
}