/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include #include #include #include #include #include #include #include #include #include #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 &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 validCache() { std::vector 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 &bytes) { std::ofstream out(path, std::ios::binary | std::ios::trunc); out.write(reinterpret_cast(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 &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; }