Files
fcitx5-m3ui/tests/testiconthemecache.cpp
T

168 lines
6.0 KiB
C++

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <random>
#include <string>
#include <vector>
#include "fcitx-utils/environ.h"
#include "fcitx-utils/log.h"
#include "fcitx-utils/standardpaths.h"
#include "fcitx/icontheme.h"
namespace fs = std::filesystem;
using fcitx::IconTheme;
using fcitx::setEnvironment;
using fcitx::StandardPaths;
using fcitx::StandardPathsOption;
void word(std::vector<uint8_t> &bytes, size_t offset, uint32_t value) {
for (int i = 3; i >= 0; --i) {
bytes.at(offset + 3 - i) = (value >> (8 * i)) & 255;
}
}
std::vector<uint8_t> validCache() {
std::vector<uint8_t> bytes(128);
bytes[1] = 1; // version 1.0
word(bytes, 4, 16); // hash table
word(bytes, 8, 48); // directory table
word(bytes, 16, 1); // buckets
word(bytes, 20, 24); // first node
word(bytes, 24, UINT32_MAX); // end of chain
word(bytes, 28, 64); // icon name
word(bytes, 32, 80); // image list
word(bytes, 48, 1);
word(bytes, 52, 96); // directory string
const std::string name = "audit-missing";
std::copy(name.begin(), name.end(), bytes.begin() + 64);
word(bytes, 80, 1); // one image in directory index zero
const std::string dir = "16x16/apps";
std::copy(dir.begin(), dir.end(), bytes.begin() + 96);
return bytes;
}
void save(const fs::path &path, const std::vector<uint8_t> &bytes) {
std::ofstream out(path, std::ios::binary | std::ios::trunc);
out.write(reinterpret_cast<const char *>(bytes.data()), bytes.size());
out.close();
FCITX_ASSERT(out.good());
fs::last_write_time(path, fs::file_time_type::clock::now() +
std::chrono::seconds(10));
}
int main() {
auto pattern = (fs::temp_directory_path() / "fcitx-icon-cache-test-XXXXXX").string();
auto *temporary = mkdtemp(pattern.data());
FCITX_ASSERT(temporary);
const fs::path root(temporary);
// The fixture is private and all recursive cleanup is limited to this root.
struct Cleanup {
fs::path root;
~Cleanup() { fs::remove_all(root); }
} cleanup{root};
setEnvironment("HOME", (root / "home").c_str());
setEnvironment("XDG_DATA_HOME", (root / "data").c_str());
setEnvironment("XDG_DATA_DIRS", (root / "empty").c_str());
StandardPaths paths("fcitx5", {}, StandardPathsOption::SkipBuiltInPath);
const auto themeDir = root / "data/icons/audit";
fs::create_directories(themeDir / "16x16/apps");
std::ofstream(themeDir / "index.theme")
<< "[Icon Theme]\nName=Audit\nDirectories=16x16/apps\n"
"[16x16/apps]\nSize=16\nType=Fixed\n";
const auto icon = themeDir / "16x16/apps/audit-missing.png";
std::ofstream(icon) << "lookup only";
const auto cache = themeDir / "icon-theme.cache";
auto check = [&](const std::vector<uint8_t> &bytes) {
save(cache, bytes);
IconTheme theme(std::string("audit"), paths);
FCITX_ASSERT(theme.findIconPath("audit-missing", 16, 1, {".png"}) == icon);
FCITX_ASSERT(theme.findIconPath("audit-missing", 16, 1, {".png"}) == icon);
};
check(validCache());
auto bytes = validCache();
// Distinguish cache-assisted lookup from a parser that always ignores it.
word(bytes, 80, 0);
save(cache, bytes);
{
IconTheme theme(std::string("audit"), paths);
FCITX_ASSERT(theme.findIconPath("audit-missing", 16, 1, {".png"}).empty());
}
for (uint32_t offset : {UINT32_MAX, UINT32_MAX - 3, uint32_t(126)}) {
bytes = validCache();
word(bytes, 4, offset);
check(bytes);
}
bytes = validCache();
word(bytes, 16, UINT32_MAX); // overflowing hash table length
check(bytes);
bytes = validCache();
word(bytes, 8, UINT32_MAX - 3); // overflowing directory table offset
check(bytes);
bytes = validCache();
word(bytes, 24, 24); // self cycle on a nonmatching name
bytes[64] = 'x';
check(bytes);
bytes = validCache();
word(bytes, 24, 36); // two-node cycle
word(bytes, 36, 24);
word(bytes, 40, 64);
word(bytes, 44, 80);
bytes[64] = 'x';
check(bytes);
bytes = validCache();
word(bytes, 80, UINT32_MAX); // overflowing image count
check(bytes);
bytes = validCache();
bytes[85] = 2; // invalid directory index
check(bytes);
bytes = validCache();
bytes.resize(1200, 'x');
word(bytes, 28, 128); // no terminating NUL in 1024 bytes
check(bytes);
for (size_t length = 0; length < 128; ++length) {
bytes = validCache();
bytes.resize(length);
check(bytes);
}
// Deterministic mutation smoke test: no assumptions about whether each
// mutated cache is valid, but neither lookup may hang or access bad memory.
std::mt19937 random(0x1c0);
for (int i = 0; i < 512; ++i) {
bytes = validCache();
for (int j = 0; j < 4; ++j) {
bytes[random() % bytes.size()] = random() & 255;
}
save(cache, bytes);
IconTheme theme(std::string("audit"), paths);
theme.findIconPath("audit-missing", 16, 1, {".png"});
theme.findIconPath("audit-missing", 32, 2, {".png"});
}
save(cache, validCache());
{
IconTheme theme(std::string("audit"), paths);
std::ofstream(cache, std::ios::trunc).close();
FCITX_ASSERT(theme.findIconPath("audit-missing", 16, 1, {".png"}) == icon);
}
fs::remove(cache);
FCITX_ASSERT(mkfifo(cache.c_str(), 0600) == 0);
{
IconTheme theme(std::string("audit"), paths);
FCITX_ASSERT(theme.findIconPath("audit-missing", 16, 1, {".png"}) == icon);
}
fs::remove(cache);
// Oversized caches are ignored without loading their contents.
save(cache, validCache());
fs::resize_file(cache, 16 * 1024 * 1024 + 1);
{
IconTheme theme(std::string("audit"), paths);
FCITX_ASSERT(theme.findIconPath("audit-missing", 16, 1, {".png"}) == icon);
}
return 0;
}