168 lines
6.0 KiB
C++
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;
|
|
}
|