✨ feat: 灵台引擎脾神(hj_transform)确定性 Markdown 解析器(WP-15)

- C ABI 新增 hj_transform_version / hj_transform_markdown(契约登记同步);HjBuffer 真实实现(头部+紧随数据)
- v1 解析(hunjing-markdown-v1):fenced code/mermaid 提取、独占行 $$ 块级数学、未闭合结构保持源码态、其余归并 markdown block;canonical JSON 键序确定、UTF-8 校验、1MiB/256 block/64KiB 行硬限额
- 引擎内部设施抽取(engine_internal.h:last_error/句柄表/buffer 共享)
- doctest 12 个用例(含 1 万次随机鲁棒性冒烟,固定种子可复现);libFuzzer 目标(-DHJ_BUILD_FUZZER,Apple clang 无 fuzzer 运行时,正式跑在 Linux CI)
- nm 验证 9 个导出符号全部 hj_*
This commit is contained in:
chenxin committed 2026-09-10 11:01:45 +08:00
1 parent d3c8df79a8
commit 127191c889
10 files changed
+565 -26

No files matched your search

-1
View File
@@ -1,5 +1,4 @@
// 灵台引擎核心测试(doctest,ADR-025)。
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
#include "hunjing/lingtai.h"
+3
View File
@@ -0,0 +1,3 @@
// doctest 入口:整个测试可执行文件只保留一份实现。
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
+139
View File
@@ -0,0 +1,139 @@
// 脾神(hj_transform)测试:确定性解析、提取、未闭合源码态、限额与校验。
#include <cstring>
#include <string>
#include <doctest/doctest.h>
#include "hunjing/lingtai.h"
namespace {
struct Engine {
HjEngine *ptr = nullptr;
Engine() { REQUIRE(hj_engine_new(HJ_ABI_VERSION, &ptr) == HJ_OK); }
~Engine() { hj_engine_free(ptr); }
};
std::string transform(const Engine &e, std::string_view input, HjStatus *status = nullptr) {
HjBuffer *buf = nullptr;
HjStatus st = hj_transform_markdown(e.ptr, input.data(), input.size(), &buf);
if (status != nullptr) *status = st;
if (st != HJ_OK) return {};
std::string out(static_cast<const char *>(hj_buffer_data(buf)), hj_buffer_len(buf));
hj_buffer_free(buf);
return out;
}
} // namespace
TEST_CASE("版本与空输入") {
Engine e;
CHECK(std::string(hj_transform_version()) == "hunjing-markdown-v1");
const std::string out = transform(e, "");
CHECK(out == R"jx({"parser_version":"hunjing-markdown-v1","blocks":[]})jx");
}
TEST_CASE("纯文本归并为 markdown block 并裁剪空白") {
Engine e;
const std::string out = transform(e, "\n你好,魂镜\n\n第二段\n\n");
CHECK(out.find(R"jx("type":"markdown")jx") != std::string::npos);
CHECK(out.find("你好,魂镜\\n\\n第二段") != std::string::npos);
CHECK(out.find(R"jx("flavor":"hunjing-gfm-v1")jx") != std::string::npos);
}
TEST_CASE("fenced code 与 mermaid 提取") {
Engine e;
const std::string input = "前置说明\n```python\nprint(1)\n```\n中间\n```mermaid\nflowchart LR\nA-->B\n```\n结尾";
const std::string out = transform(e, input);
CHECK(out.find(R"jx("type":"code","source":"print(1)","language":"python")jx") != std::string::npos);
CHECK(out.find(R"jx("type":"mermaid","source":"flowchart LR\nA-->B")jx") != std::string::npos);
CHECK(out.find("前置说明") != std::string::npos);
CHECK(out.find("结尾") != std::string::npos);
}
TEST_CASE("块级数学定界提取") {
Engine e;
const std::string input = "前文\n$$\nE=mc^2\n$$\n后文";
const std::string out = transform(e, input);
CHECK(out.find(R"jx("type":"math","notation":"latex","display":"block","source":"E=mc^2")jx") != std::string::npos);
}
TEST_CASE("未闭合 fenced 保持源码态") {
Engine e;
const std::string input = "开头\n```js\nunterminated(";
const std::string out = transform(e, input);
CHECK(out.find(R"jx("type":"markdown")jx") != std::string::npos);
CHECK(out.find("```js") != std::string::npos);
CHECK(out.find("unterminated(") != std::string::npos);
CHECK(out.find(R"jx("type":"code")jx") == std::string::npos);
}
TEST_CASE("未闭合数学定界保持源码态") {
Engine e;
const std::string out = transform(e, "a\n$$\nunclosed math");
CHECK(out.find(R"jx("type":"math")jx") == std::string::npos);
CHECK(out.find("$$") != std::string::npos);
}
TEST_CASE("确定性:同输入两次输出字节一致") {
Engine e;
const std::string input = "x\n```mermaid\nA-->B\n```\n$$\nz\n$$\n";
const std::string a = transform(e, input);
const std::string b = transform(e, input);
CHECK(a == b);
}
TEST_CASE("非法 UTF-8 拒绝") {
Engine e;
HjStatus st = HJ_OK;
transform(e, std::string_view("ab\xff\xfe", 5), &st);
CHECK(st == HJ_ERR_INVALID_ARGUMENT);
}
TEST_CASE("超长输入拒绝") {
Engine e;
const std::string big((1 << 20) + 1, 'a');
HjStatus st = HJ_OK;
transform(e, big, &st);
CHECK(st == HJ_ERR_LIMIT_EXCEEDED);
}
TEST_CASE("参数与句柄校验") {
Engine e;
HjBuffer *buf = nullptr;
CHECK(hj_transform_markdown(e.ptr, "x", 1, nullptr) == HJ_ERR_INVALID_ARGUMENT);
CHECK(hj_transform_markdown(nullptr, "x", 1, &buf) == HJ_ERR_HANDLE_INVALID);
CHECK(hj_transform_markdown(e.ptr, nullptr, 5, &buf) == HJ_ERR_INVALID_ARGUMENT);
}
TEST_CASE("随机鲁棒性:任意字节输入不崩溃且返回已知状态码") {
// 本地等价冒烟:正式 libFuzzer 目标(fuzz/fuzz_transform.cpp)在 Linux CI 运行。
Engine e;
uint64_t rng = 0x9E3779B97F4A7C15ULL; // 固定种子,可复现
auto next = [&rng](uint64_t bound) {
rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
return static_cast<size_t>(rng % bound);
};
static const char *kPieces[] = {
"text ", "\n", "```", "mermaid\n", "python\n", "$$\n", "`", "$",
"hello", "\xE4\xB8\xAD", "\"", "\\", "flowchart LR\nA-->B\n", "\x7f",
};
for (int iter = 0; iter < 10000; ++iter) {
std::string input;
if (iter % 2 == 0) {
const size_t len = next(512);
input.reserve(len);
for (size_t k = 0; k < len; ++k) input.push_back(static_cast<char>(next(256)));
} else {
for (size_t k = 0, m = next(24); k < m; ++k) input += kPieces[next(14)];
}
HjBuffer *buf = nullptr;
const HjStatus st = hj_transform_markdown(e.ptr, input.data(), input.size(), &buf);
CHECK((st == HJ_OK || st == HJ_ERR_INVALID_ARGUMENT || st == HJ_ERR_LIMIT_EXCEEDED ||
st == HJ_ERR_PARSE_FAILED));
if (buf != nullptr) {
CHECK(hj_buffer_data(buf) != nullptr);
CHECK(hj_buffer_free(buf) == HJ_OK);
}
}
}