Use native candidate actions and harden frontend resource loading

This commit is contained in:
justme committed 2026-10-20 11:52:39 +08:00
1 parent 99fe8f589f
commit d8e780dfa1
18 files changed
+1083 -227

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
/* 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;
}
+63
View File
@@ -0,0 +1,63 @@
#include <memory>
#include <vector>
#include "fcitx-utils/log.h"
#include "fcitx/candidatelist.h"
#include "../src/candidatemenu.h"
using namespace fcitx;
using namespace fcitx::m3ui;
namespace {
class EngineActions : public ActionableCandidateList {
public:
explicit EngineActions(bool available) : available_(available) {}
bool hasAction(const CandidateWord &) const override { return available_; }
std::vector<CandidateAction>
candidateActions(const CandidateWord &) const override {
CandidateAction action;
action.setId(-42); // IDs must not be interpreted by the UI.
action.setText("Engine action");
CandidateAction separator;
separator.setSeparator(true);
return {separator, action};
}
void triggerAction(const CandidateWord &, int) override {}
private:
bool available_;
};
} // namespace
int main() {
CommonCandidateList list;
list.setPageSize(5);
list.append<DisplayOnlyCandidateWord>(Text("候选词"));
list.append<DisplayOnlyCandidateWord>(Text("保留的词"));
// Never invent a blocking action when ranking operations are unavailable.
auto menu = candidateMenu(list, list.candidate(0));
FCITX_ASSERT(menu.actions.empty());
// An engine can have an action interface but no actions for this word.
list.setActionableImpl(std::make_unique<EngineActions>(false));
menu = candidateMenu(list, list.candidate(0));
FCITX_ASSERT(menu.actions.empty());
// Keep engine actions and IDs intact; omit separators, never append blocking.
list.setActionableImpl(std::make_unique<EngineActions>(true));
menu = candidateMenu(list, list.candidate(0));
FCITX_ASSERT(menu.actions.size() == 1);
FCITX_ASSERT(menu.actions[0].id() == -42);
FCITX_ASSERT(menu.actions[0].text() == "Engine action");
FCITX_ASSERT(list.size() == 2);
FCITX_ASSERT(list.candidate(0).text().toString() == "候选词");
// Do not offer an ineffective block action for a read-only list.
DisplayOnlyCandidateList readOnly;
readOnly.setContent(std::vector<std::string>{"只读"});
menu = candidateMenu(readOnly, readOnly.candidate(0));
FCITX_ASSERT(menu.actions.empty());
}
+77
View File
@@ -0,0 +1,77 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <cstdint>
#include <string>
#include <vector>
#include "fcitx-utils/log.h"
#include "../src/imageloader.h"
using namespace fcitx::m3ui;
int main() {
FCITX_ASSERT(!loadRasterImage({}));
FCITX_ASSERT(!loadSvgImage({}));
FCITX_ASSERT(!loadRasterImage({'b', 'a', 'd'}));
FCITX_ASSERT(!loadSvgImage({'b', 'a', 'd'}));
// Valid PNG bytes exercise the real incremental decoder, regardless of
// extension. The reference must remain valid after loader destruction.
LoadedSurface surface(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 16, 16));
std::vector<uint8_t> png;
FCITX_ASSERT(cairo_surface_write_to_png_stream(
surface.get(),
+[](void *data, const unsigned char *bytes, unsigned int length) {
auto &out = *static_cast<std::vector<uint8_t> *>(data);
out.insert(out.end(), bytes, bytes + length);
return CAIRO_STATUS_SUCCESS;
},
&png) == CAIRO_STATUS_SUCCESS);
auto image = loadRasterImage(png);
FCITX_ASSERT(image);
FCITX_ASSERT(gdk_pixbuf_get_width(image.get()) == 16);
FCITX_ASSERT(gdk_pixbuf_get_height(image.get()) == 16);
png[16] = 255; // Huge IHDR, rejected before entering libpng.
FCITX_ASSERT(!loadRasterImage(png));
png.resize(23);
FCITX_ASSERT(!loadRasterImage(png));
// A real non-PNG image exercises size-prepared rather than PNG preflight.
gchar *bmpData = nullptr;
gsize bmpSize = 0;
FCITX_ASSERT(gdk_pixbuf_save_to_buffer(image.get(), &bmpData, &bmpSize,
"bmp", nullptr, nullptr));
std::vector<uint8_t> bmp(bmpData, bmpData + bmpSize);
g_free(bmpData);
FCITX_ASSERT(loadRasterImage(bmp));
// BMP BITMAPINFOHEADER width is little-endian at byte 18.
FCITX_ASSERT(bmp.size() > 26 && bmp[14] >= 40);
bmp[18] = 1;
bmp[19] = 32; // 8193 pixels wide
bmp[20] = bmp[21] = 0;
FCITX_ASSERT(!loadRasterImage(bmp));
auto svg = [](const std::string &text) {
return loadSvgImage(std::vector<uint8_t>(text.begin(), text.end()));
};
auto rendered = svg("<svg xmlns='http://www.w3.org/2000/svg' width='16' "
"height='16'><rect width='16' height='16' "
"fill='red'/></svg>");
FCITX_ASSERT(rendered);
FCITX_ASSERT(cairo_surface_get_type(rendered.get()) == CAIRO_SURFACE_TYPE_IMAGE);
FCITX_ASSERT(cairo_image_surface_get_width(rendered.get()) == 16);
FCITX_ASSERT(cairo_image_surface_get_height(rendered.get()) == 16);
cairo_surface_flush(rendered.get());
const auto *pixel = reinterpret_cast<const uint32_t *>(
cairo_image_surface_get_data(rendered.get()));
FCITX_ASSERT(pixel[0] == 0xffff0000); // Native-endian Cairo ARGB32.
FCITX_ASSERT(!svg("<svg xmlns='http://www.w3.org/2000/svg' "
"width='1e30' height='16'/>"));
FCITX_ASSERT(!svg("<svg xmlns='http://www.w3.org/2000/svg' "
"width='8192' height='8192'/>"));
FCITX_ASSERT(!svg("<svg xmlns='http://www.w3.org/2000/svg' "
"width='0' height='16'/>"));
FCITX_ASSERT(!svg("<svg xmlns='http://www.w3.org/2000/svg'"));
std::vector<uint8_t> oversized(16 * 1024 * 1024 + 1);
FCITX_ASSERT(!loadRasterImage(oversized));
FCITX_ASSERT(!loadSvgImage(oversized));
}
+63
View File
@@ -0,0 +1,63 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <unistd.h>
#include <cstdlib>
#include <filesystem>
#include <limits>
#include "fcitx-utils/log.h"
#include "../src/resourceguard.h"
using namespace fcitx::m3ui;
int main() {
FCITX_ASSERT(safeImageSize(1, 1));
FCITX_ASSERT(safeImageSize(4096, 4096));
FCITX_ASSERT(safeImageSize(8192, 128));
FCITX_ASSERT(!safeImageSize(0, 16));
FCITX_ASSERT(!safeImageSize(-1, 16));
FCITX_ASSERT(!safeImageSize(8193, 16));
FCITX_ASSERT(!safeImageSize(8192, 8192));
FCITX_ASSERT(!safeImageSize(4096.1, 4096));
FCITX_ASSERT(!safeImageSize(std::numeric_limits<double>::infinity(), 16));
FCITX_ASSERT(!safeImageSize(std::numeric_limits<double>::quiet_NaN(), 16));
FCITX_ASSERT(safeImageMargins(16, 16, 0, 0, 0, 0));
FCITX_ASSERT(safeImageMargins(16, 16, 8, 8, 8, 8));
FCITX_ASSERT(!safeImageMargins(16, 16, 9, 8, 8, 8));
FCITX_ASSERT(!safeImageMargins(16, 16, -1, 0, 0, 0));
FCITX_ASSERT(!safeImageMargins(0, 16, 0, 0, 0, 0));
FCITX_ASSERT(!safeImageMargins(16, 16,
std::numeric_limits<int>::max(), 0, 1, 0));
std::vector<uint8_t> png = {137, 80, 78, 71, 13, 10, 26, 10,
0, 0, 0, 13, 'I', 'H', 'D', 'R',
0, 0, 0, 16, 0, 0, 0, 16};
FCITX_ASSERT(safePngSize(png));
png[16] = 255;
FCITX_ASSERT(!safePngSize(png));
png[16] = 0;
png[19] = 0;
FCITX_ASSERT(!safePngSize(png));
png.resize(23);
FCITX_ASSERT(!safePngSize(png));
FCITX_ASSERT(!readImageSnapshot(fcitx::UnixFD{}));
auto name = (std::filesystem::temp_directory_path() /
"fcitx-image-test-XXXXXX").string();
fcitx::UnixFD file = fcitx::UnixFD::own(mkstemp(name.data()));
FCITX_ASSERT(file.isValid());
// Unlink immediately; the descriptor owns this isolated fixture.
FCITX_ASSERT(unlink(name.c_str()) == 0);
FCITX_ASSERT(!readImageSnapshot(file));
FCITX_ASSERT(write(file.fd(), "image", 5) == 5);
FCITX_ASSERT(lseek(file.fd(), 0, SEEK_SET) == 0);
auto bytes = readImageSnapshot(file);
FCITX_ASSERT(bytes && bytes->size() == 5);
// A short read (e.g. concurrent truncation) is rejected.
FCITX_ASSERT(lseek(file.fd(), 3, SEEK_SET) == 3);
FCITX_ASSERT(!readImageSnapshot(file));
FCITX_ASSERT(ftruncate(file.fd(), maxImageFileSize + 1) == 0);
FCITX_ASSERT(!readImageSnapshot(file));
int fds[2];
FCITX_ASSERT(pipe(fds) == 0);
fcitx::UnixFD readEnd = fcitx::UnixFD::own(fds[0]);
fcitx::UnixFD writeEnd = fcitx::UnixFD::own(fds[1]);
FCITX_ASSERT(!readImageSnapshot(readEnd));
}