Use native candidate actions and harden frontend resource loading
This commit is contained in:
1 parent
99fe8f589f
commit
d8e780dfa1
18 files changed
+1083
-227
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user