Files
msime-windows/engine/contracts/tests/windows_ipc_contract.cpp
T
fanlusky 233ac9ef33 feat(engine,server,windows,ui-html,installer): 句中辅助码触发键可多选,新增分号
设置页「句中辅助码触发键」可勾选反引号和分号,至少保留一个。

- 分号只在 AcceptsMarkerAt 成立(这一节是偶数键)时才当触发键;微软/搜狗/紫光双拼的
  ing 韵母只跟在奇数键之后,两者不抢同一个位置,判断时分号先让给 ing 韵母
- 无论按哪个键,TSF 按键缓冲、Server raw 与引擎输入串里都记成反引号,引擎解析不变
- 新配置项 helpcode.shuangpin_mid_sentence_helpcode_backtick / _semicolon 同步写入出厂
  模板、工作配置与两个设置宿主
- opcode 28 保持「反引号是否触发」、载荷单个 "0"/"1"(TSF 会丢弃更长的帧);分号走新增
  的 opcode 29,旧 DLL 忽略它
2026-10-02 23:55:46 +08:00

192 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "../ipc_negotiation.h"
#include "../mid_sentence_helpcode.h"
#include "../voice_composition_pipe.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <iostream>
#define CHECK(expression) \
do \
{ \
if (!(expression)) \
{ \
std::cerr << #expression << " at " << __LINE__ << '\n'; \
return 1; \
} \
} while (false)
int main()
{
// Wire bytes, not just matching C++ declarations: existing v1 DLLs write
// these offsets on both x86 and x64. Changes must not move UTF-16 data.
std::array<unsigned char, 304> bytes{};
bytes[0] = 10; // ClientHello
bytes[8] = 7; // client id
FanyImeNamedpipeData legacy{};
std::memcpy(&legacy, bytes.data(), bytes.size());
CHECK(legacy.client_id == 7);
CHECK(FanyImePipeEventType::HideCaretState == 16);
CHECK(FanyImePipeEventType::HideCaretState != FanyImePipeEventType::HideCandidateWnd);
CHECK(!FanyImePipeEventType::IsRouteDeactivation(FanyImePipeEventType::HideCaretState));
CHECK(FanyImeProtocol::CaretStateIndicator == (1u << 5));
CHECK((FanyImeProtocol::CaretStateIndicator & FanyImeProtocol::RequiredCapabilities) == 0);
// CapsLockEdge keeps the VK_CAPITAL value the first Servers matched on.
CHECK(FanyImeCaretStateTrigger::UserToggle == 0);
CHECK(FanyImeCaretStateTrigger::CapsLockEdge == 0x14);
CHECK(FanyImeCaretStateTrigger::FocusEntered != FanyImeCaretStateTrigger::UserToggle);
CHECK(FanyImeCaretStateTrigger::FocusEntered != FanyImeCaretStateTrigger::CapsLockEdge);
CHECK(FanyImeProtocol::Negotiate(legacy).legacy);
CHECK(FanyImeProtocol::Negotiate(legacy).accepted);
const auto capsOffSnapshot = FanyImePipeFlags::EncodeImeSwitchCapsLockSnapshot(false);
const auto capsOnSnapshot = FanyImePipeFlags::EncodeImeSwitchCapsLockSnapshot(true);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotPresent & FanyImePipeFlags::UiLess) == 0);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled & FanyImePipeFlags::UiLess) == 0);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotPresent & 0xffu) == 0);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled & 0xffu) == 0);
CHECK(FanyImePipeFlags::HasImeSwitchCapsLockSnapshot(capsOffSnapshot));
CHECK(!FanyImePipeFlags::ImeSwitchCapsLockSnapshotEnabled(capsOffSnapshot));
CHECK(FanyImePipeFlags::HasImeSwitchCapsLockSnapshot(capsOnSnapshot));
CHECK(FanyImePipeFlags::ImeSwitchCapsLockSnapshotEnabled(capsOnSnapshot));
CHECK(!FanyImePipeFlags::HasImeSwitchCapsLockSnapshot(0)); // old DLL packet
CHECK(!FanyImePipeFlags::ImeSwitchCapsLockSnapshotEnabled(FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled));
CHECK(FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOffSnapshot).has_value());
CHECK(!*FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOffSnapshot));
CHECK(*FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOnSnapshot));
CHECK(!FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(0).has_value());
CHECK(
!FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled).has_value());
auto hello = FanyImeProtocol::Hello(7, 19);
auto result = FanyImeProtocol::Negotiate(hello);
CHECK(result.accepted && !result.legacy);
auto reply = FanyImeProtocol::Reply(hello, result);
CHECK(FanyImeProtocol::AcceptReply(reply, 19));
CHECK(!FanyImeProtocol::AcceptReply(reply, 18)); // stale reconnect ACK
CHECK(!FanyImeProtocol::AcceptReply(reply, 0));
const auto shortcutCapabilities = FanyImeProtocol::Capabilities | FanyImeProtocol::CharacterSetShortcut;
const auto shortcutHello = FanyImeProtocol::Hello(7, 20, shortcutCapabilities);
const auto oldServer = FanyImeProtocol::Negotiate(shortcutHello);
CHECK(oldServer.accepted);
CHECK((oldServer.capabilities & FanyImeProtocol::CharacterSetShortcut) == 0);
const auto newServer = FanyImeProtocol::Negotiate(shortcutHello, shortcutCapabilities);
CHECK(newServer.accepted);
CHECK((newServer.capabilities & FanyImeProtocol::CharacterSetShortcut) != 0);
const auto shortcutReply = FanyImeProtocol::Reply(shortcutHello, newServer);
CHECK(FanyImeProtocol::AcceptReply(shortcutReply, 20));
CHECK(FanyImeProtocol::ReplyCapabilities(shortcutReply) == shortcutCapabilities);
CHECK((FanyImeProtocol::Negotiate(hello, shortcutCapabilities).capabilities &
FanyImeProtocol::CharacterSetShortcut) == 0); // old client/new server
CHECK(FanyImeProtocol::IsCharacterSetShortcut('F', 3));
CHECK(FanyImeProtocol::IsCharacterSetShortcut('F', 0x80000003u));
for (unsigned modifiers = 0; modifiers < 8; ++modifiers)
CHECK(FanyImeProtocol::IsCharacterSetShortcut('F', modifiers) == (modifiers == 3));
CHECK(!FanyImeProtocol::IsCharacterSetShortcut('E', 3));
// Backspace retraction is negotiated separately: an old Server must keep
// its current behavior, and an old DLL must never receive CompositionRestored.
const auto restoreCapabilities = FanyImeProtocol::Capabilities | FanyImeProtocol::CompositionRestore;
const auto restoreHello = FanyImeProtocol::Hello(7, 21, restoreCapabilities);
const auto oldServerForRestore = FanyImeProtocol::Negotiate(restoreHello);
CHECK(oldServerForRestore.accepted);
CHECK((oldServerForRestore.capabilities & FanyImeProtocol::CompositionRestore) == 0);
const auto newServerForRestore = FanyImeProtocol::Negotiate(restoreHello, restoreCapabilities);
CHECK(newServerForRestore.accepted);
CHECK((newServerForRestore.capabilities & FanyImeProtocol::CompositionRestore) != 0);
CHECK(FanyImeProtocol::AcceptReply(FanyImeProtocol::Reply(restoreHello, newServerForRestore), 21));
CHECK(FanyImeProtocol::ReplyCapabilities(FanyImeProtocol::Reply(restoreHello, newServerForRestore)) ==
restoreCapabilities);
CHECK((FanyImeProtocol::Negotiate(hello, restoreCapabilities).capabilities & FanyImeProtocol::CompositionRestore) ==
0); // old client/new server
CHECK(FanyImeReplyType::CompositionRestored == 14);
CHECK(FanyImeReplyType::MaxKnown == FanyImeReplyType::CompositionRestored);
CHECK(FanyImeReplyType::TransportUnavailable > FanyImeReplyType::MaxKnown);
hello.wch += 1;
result = FanyImeProtocol::Negotiate(hello);
CHECK(!result.accepted);
CHECK(!FanyImeProtocol::AcceptReply(FanyImeProtocol::Reply(hello, result), 19));
hello = FanyImeProtocol::Hello(7, 19);
hello.point[1] |= 1u << 20; // client requires an unknown capability
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
hello = FanyImeProtocol::Hello(7, 19);
hello.modifiers_down |= 1u << 20; // unknown optional capabilities are ignored
CHECK(FanyImeProtocol::Negotiate(hello).accepted);
hello.modifiers_down &= ~FanyImeProtocol::FocusEpochs;
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
hello = FanyImeProtocol::Hello(7, 19);
hello.keycode ^= 1;
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
hello = FanyImeProtocol::Hello(7, 0);
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
FanyImeNamedpipeDataToTsf old_server{};
old_server.msg_type = FanyImeReplyType::PipeReady;
old_server.request_id = 19;
CHECK(!FanyImeProtocol::AcceptReply(old_server, 19));
// Corrupt/missing negotiated capabilities cannot authorize the new DLL.
reply.candidate_string[2] = 0;
CHECK(!FanyImeProtocol::AcceptReply(reply, 19));
CHECK(FanyImeWorkerReplyType::SwitchToEn == FanyImeWorkerReplyType::SwitchToEnglish);
CHECK(FanyImeWorkerReplyType::CommitCandidate == FanyImeWorkerReplyType::CommitCurCandidate);
CHECK(FanyImeWorkerReplyType::CommitCandidateAndContinue == 27);
CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeChanged == 28);
CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeSemicolonChanged == 29);
CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::MidSentenceHelpcodeSemicolonChanged);
// TSF(WCHAR)与引擎(char)共用同一条句中辅助码形状规则。
const auto accepts = [](const std::wstring &text) {
return FanyImeMidSentenceHelpcode::AcceptsMarker(text.data(), text.size());
};
CHECK(accepts(L"ulpb") && !accepts(L"ulp") && !accepts(L"") && !accepts(L"ulpb'"));
CHECK(!accepts(L"ulpb`") && !accepts(L"ulpb`x") && !accepts(L"ulpb`xY") && !accepts(L"ulpb`xi"));
CHECK(accepts(L"ulpb`xih") && !accepts(L"ulpb`xihf") && accepts(L"ulpb`xYih") && accepts(L"ulpb`xihfa"));
CHECK(FanyImeMidSentenceHelpcode::AcceptsMarker("ni'hc", 5) &&
!FanyImeMidSentenceHelpcode::AcceptsMarker("ni'h", 4));
// 光标移回句中:只看光标前的部分,光标后已是这个音节的段时不收。
const auto accepts_at = [](const std::wstring &text, std::size_t caret) {
return FanyImeMidSentenceHelpcode::AcceptsMarkerAt(text.data(), text.size(), caret);
};
CHECK(accepts_at(L"ulpbih", 2) && accepts_at(L"ulpbih", 4) && accepts_at(L"ulpbih", 6));
CHECK(!accepts_at(L"ulpbih", 0) && !accepts_at(L"ulpbih", 3) && !accepts_at(L"ulpbih", 7));
CHECK(!accepts_at(L"ul`xpb", 2) && accepts_at(L"ul`xpb", 6) && !accepts_at(L"ul`xpb", 3));
const std::wstring voice(1000, L'x');
const auto frames = FanyImeVoiceCompositionPipe::EncodeSnapshot(voice, 7);
CHECK(FanyImeVoiceCompositionPipe::AssembleFrames(frames) == voice);
auto incomplete = frames;
incomplete.erase(incomplete.begin());
CHECK(FanyImeVoiceCompositionPipe::AssembleFrames(incomplete).empty());
// A middle frame legally carries flags 0, so an unterminated packet must be rejected on the chunk, not accepted
// because data[0] happens to be a NUL.
std::array<wchar_t, FanyImeVoiceCompositionPipe::kPacketChars> packet{};
packet.fill(L'x');
packet[0] = 0; // middle frame
packet[1] = 7; // generation
CHECK(!FanyImeVoiceCompositionPipe::ParseFrame(packet.data()).valid);
packet[FanyImeVoiceCompositionPipe::kPacketChars - 1] = 0;
const auto middle = FanyImeVoiceCompositionPipe::ParseFrame(packet.data());
CHECK(middle.valid && !middle.first && !middle.last && middle.generation == 7);
CHECK(middle.chunk == std::wstring(FanyImeVoiceCompositionPipe::kMaxChunkChars, L'x'));
const auto blank = FanyImeVoiceCompositionPipe::EncodeSnapshot(std::wstring(), 7);
CHECK(blank.size() == 1);
std::array<wchar_t, FanyImeVoiceCompositionPipe::kPacketChars> header{};
std::copy(blank[0].begin(), blank[0].end(), header.begin());
const auto empty = FanyImeVoiceCompositionPipe::ParseFrame(header.data());
CHECK(empty.valid && empty.first && empty.last && empty.chunk.empty());
// Statistics frames: fixed 28-byte header plus 24-byte events; the Go stats
// tool mirrors these numbers in internal/frames, so both sides must change
// together.
CHECK(FANY_IME_STATS_MAGIC == 0x54415453);
CHECK(FANY_IME_STATS_VERSION == 1);
CHECK(sizeof(FanyImeStatsBatchHeader) == 28);
CHECK(sizeof(FanyImeStatsEvent) == 24);
CHECK(offsetof(FanyImeStatsEvent, timestamp_utc_ft) == 0);
CHECK(offsetof(FanyImeStatsEvent, cjk) == 8);
CHECK(sizeof(FanyImeStatsBatchHeader) + 256 * sizeof(FanyImeStatsEvent) <= FANY_IME_STATS_MAX_FRAME_BYTES);
std::cout << "Windows IPC layout, negotiation, upgrade, voice and stats contracts passed\n";
}