✨ 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 -1
View File
@@ -111,7 +111,7 @@ docker-compose.override.yml
target/ target/
# C++ 引擎与契约生成产物(源码构建可再生) # C++ 引擎与契约生成产物(源码构建可再生)
engine/build/ engine/build*/
openapi/bundled.json openapi/bundled.json
**/*.rs.bk **/*.rs.bk
sandbox-work/ sandbox-work/
+18 -3
View File
@@ -17,14 +17,29 @@ endif()
set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
add_library(hunjing SHARED src/lingtai.cpp) add_library(hunjing SHARED src/lingtai.cpp src/transform.cpp)
target_include_directories(hunjing PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(hunjing
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
# 单元测试(doctest,ADR-025);单头直接引入,不用 FetchContent 拉网络 # 单元测试(doctest,ADR-025);单头直接引入,不用 FetchContent 拉网络
add_executable(lingtai_tests tests/test_core.cpp src/lingtai.cpp) add_executable(lingtai_tests tests/test_main.cpp tests/test_core.cpp tests/test_transform.cpp
src/lingtai.cpp src/transform.cpp)
target_include_directories(lingtai_tests PRIVATE target_include_directories(lingtai_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/third_party) ${CMAKE_CURRENT_SOURCE_DIR}/third_party)
enable_testing() enable_testing()
add_test(NAME lingtai_tests COMMAND lingtai_tests) add_test(NAME lingtai_tests COMMAND lingtai_tests)
# fuzz 目标(ADR-025:hj_transform 为必测目标;需要 clang libFuzzer)
option(HJ_BUILD_FUZZER "Build libFuzzer targets" OFF)
if(HJ_BUILD_FUZZER)
add_executable(fuzz_transform fuzz/fuzz_transform.cpp src/lingtai.cpp src/transform.cpp)
target_include_directories(fuzz_transform PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(fuzz_transform PRIVATE -fsanitize=fuzzer,address -g)
target_link_options(fuzz_transform PRIVATE -fsanitize=fuzzer,address)
endif()
+28
View File
@@ -0,0 +1,28 @@
// hj_transform fuzz 目标(ADR-025:libFuzzer 必测)。
// 构建:cmake -DHJ_BUILD_FUZZER=ON(需要 clang 的 -fsanitize=fuzzer)。
#include <cstdint>
#include <cstring>
#include "hunjing/lingtai.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
HjEngine *engine = nullptr;
if (hj_engine_new(HJ_ABI_VERSION, &engine) != HJ_OK) {
return 0;
}
HjBuffer *buf = nullptr;
const HjStatus st = hj_transform_markdown(engine, reinterpret_cast<const char *>(data), size, &buf);
if (st == HJ_OK) {
// 输出必须可读且与声称长度一致
const void *p = hj_buffer_data(buf);
const size_t len = hj_buffer_len(buf);
if (p == nullptr || len == 0) {
hj_buffer_free(buf);
hj_engine_free(engine);
__builtin_trap();
}
hj_buffer_free(buf);
}
hj_engine_free(engine);
return 0;
}
+11
View File
@@ -65,6 +65,17 @@ HJ_API size_t hj_buffer_len(const HjBuffer *buffer);
// 线程局部的最近一次错误消息(UTF-8、脱敏、不含正文与密钥);下一次调用覆盖。 // 线程局部的最近一次错误消息(UTF-8、脱敏、不含正文与密钥);下一次调用覆盖。
HJ_API const char *hj_last_error(void); HJ_API const char *hj_last_error(void);
// ===== 脾神(hj_transform):Markdown → canonical ContentBlock 确定性解析 =====
// 返回当前 parser version(稳定标识,随实现演进递增)。
HJ_API const char *hj_transform_version(void);
// 将 Markdown 规范化为 canonical ContentBlock JSON(键序确定、UTF-8)。
// 输出经 HjBuffer 返回,由 hj_buffer_free 释放;blocks 不含 id(由宿主生成)。
// 限额与非法 UTF-8 分别返回 HJ_ERR_LIMIT_EXCEEDED / HJ_ERR_INVALID_ARGUMENT。
HJ_API HjStatus hj_transform_markdown(
HjEngine *engine, const char *input, size_t input_len, HjBuffer **out);
#ifdef __cplusplus #ifdef __cplusplus
} // extern "C" } // extern "C"
#endif #endif
+26
View File
@@ -0,0 +1,26 @@
// 灵台引擎内部共享设施(非公共契约,仅引擎源文件使用)。
#ifndef HUNJING_LINGTAI_INTERNAL_H
#define HUNJING_LINGTAI_INTERNAL_H
#include <mutex>
#include <string>
#include <string_view>
#include <unordered_set>
#include "hunjing/lingtai.h"
// HjBuffer 布局:头部 + 紧随的字节数据(data = buffer + 1)。
struct HjBuffer {
size_t len;
};
// 线程局部错误消息。
void hj_internal_set_error(const char *message);
// 句柄表:活跃引擎登记与校验(线程安全)。
bool hj_internal_engine_valid(HjEngine *engine);
// 分配输出缓冲;失败返回 nullptr(由调用方映射为 HJ_ERR_OUT_OF_MEMORY)。
HjBuffer *hj_internal_buffer_new(std::string_view data);
#endif // HUNJING_LINGTAI_INTERNAL_H
+34 -21
View File
@@ -1,55 +1,68 @@
// 灵台引擎核心(太一):版本协商、句柄表与错误上下文的最小实现。 // 灵台引擎核心(太一):版本协商、句柄表、缓冲与错误上下文的实现。
// 全部入口按不可信输入处理;异常不得跨越 ABI 边界。 // 全部入口按不可信输入处理;异常不得跨越 ABI 边界。
#include "hunjing/lingtai.h" #include "hunjing/lingtai.h"
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_set> #include <unordered_set>
// 不透明句柄的实际布局(不进入契约;仅本翻译单元可见字段)。 #include "engine_internal.h"
// 不透明句柄的实际布局(仅本翻译单元可见字段)。
struct HjEngine { struct HjEngine {
uint32_t abi_major; uint32_t abi_major;
}; };
struct HjBuffer {
size_t len;
};
namespace { namespace {
// 线程局部错误消息(脱敏、UTF-8);下一次引擎调用覆盖。
thread_local std::string g_last_error; thread_local std::string g_last_error;
void set_last_error(const char *message) { g_last_error = message; }
// 句柄表:登记活跃引擎句柄,支撑二次释放与非法句柄检测(不崩溃)。
std::mutex g_handle_table_mutex; std::mutex g_handle_table_mutex;
std::unordered_set<HjEngine *> g_handle_table; std::unordered_set<HjEngine *> g_handle_table;
} // namespace } // namespace
void hj_internal_set_error(const char *message) { g_last_error = message; }
bool hj_internal_engine_valid(HjEngine *engine) {
const std::lock_guard<std::mutex> lock(g_handle_table_mutex);
return g_handle_table.find(engine) != g_handle_table.end();
}
HjBuffer *hj_internal_buffer_new(std::string_view data) {
void *mem = std::malloc(sizeof(HjBuffer) + data.size());
if (mem == nullptr) return nullptr;
auto *buf = static_cast<HjBuffer *>(mem);
buf->len = data.size();
if (!data.empty()) {
std::memcpy(buf + 1, data.data(), data.size());
}
return buf;
}
extern "C" { extern "C" {
uint32_t hj_abi_version(void) { return HJ_ABI_VERSION; } uint32_t hj_abi_version(void) { return HJ_ABI_VERSION; }
HjStatus hj_engine_new(uint32_t required_abi_major, HjEngine **out_engine) { HjStatus hj_engine_new(uint32_t required_abi_major, HjEngine **out_engine) {
try { try {
set_last_error(""); g_last_error.clear();
if (out_engine == nullptr) { if (out_engine == nullptr) {
set_last_error("out_engine 不能为空"); g_last_error = "out_engine 不能为空";
return HJ_ERR_INVALID_ARGUMENT; return HJ_ERR_INVALID_ARGUMENT;
} }
*out_engine = nullptr; *out_engine = nullptr;
if (required_abi_major != HJ_ABI_VERSION) { if (required_abi_major != HJ_ABI_VERSION) {
set_last_error("ABI major 不匹配:拒绝初始化,不静默降级"); g_last_error = "ABI major 不匹配:拒绝初始化,不静默降级";
return HJ_ERR_UNSUPPORTED_ABI; return HJ_ERR_UNSUPPORTED_ABI;
} }
auto *engine = new (std::nothrow) HjEngine{required_abi_major}; auto *engine = new (std::nothrow) HjEngine{required_abi_major};
if (engine == nullptr) { if (engine == nullptr) {
set_last_error("引擎内存分配失败"); g_last_error = "引擎内存分配失败";
return HJ_ERR_OUT_OF_MEMORY; return HJ_ERR_OUT_OF_MEMORY;
} }
{ {
@@ -60,40 +73,40 @@ HjStatus hj_engine_new(uint32_t required_abi_major, HjEngine **out_engine) {
return HJ_OK; return HJ_OK;
} catch (...) { } catch (...) {
// 入口捕获一切异常并转换为 HjStatus(契约禁令)。 // 入口捕获一切异常并转换为 HjStatus(契约禁令)。
set_last_error("引擎内部错误"); g_last_error = "引擎内部错误";
return HJ_ERR_INTERNAL; return HJ_ERR_INTERNAL;
} }
} }
HjStatus hj_engine_free(HjEngine *engine) { HjStatus hj_engine_free(HjEngine *engine) {
try { try {
set_last_error(""); g_last_error.clear();
{ {
const std::lock_guard<std::mutex> lock(g_handle_table_mutex); const std::lock_guard<std::mutex> lock(g_handle_table_mutex);
if (g_handle_table.erase(engine) == 0) { if (g_handle_table.erase(engine) == 0) {
set_last_error("句柄不在登记表:已释放或非法"); g_last_error = "句柄不在登记表:已释放或非法";
return HJ_ERR_HANDLE_INVALID; return HJ_ERR_HANDLE_INVALID;
} }
} }
delete engine; delete engine;
return HJ_OK; return HJ_OK;
} catch (...) { } catch (...) {
set_last_error("引擎内部错误"); g_last_error = "引擎内部错误";
return HJ_ERR_INTERNAL; return HJ_ERR_INTERNAL;
} }
} }
HjStatus hj_buffer_free(HjBuffer *buffer) { HjStatus hj_buffer_free(HjBuffer *buffer) {
try { try {
set_last_error(""); g_last_error.clear();
if (buffer == nullptr) { if (buffer == nullptr) {
set_last_error("buffer 句柄为空"); g_last_error = "buffer 句柄为空";
return HJ_ERR_INVALID_ARGUMENT; return HJ_ERR_INVALID_ARGUMENT;
} }
free(buffer); free(buffer);
return HJ_OK; return HJ_OK;
} catch (...) { } catch (...) {
set_last_error("引擎内部错误"); g_last_error = "引擎内部错误";
return HJ_ERR_INTERNAL; return HJ_ERR_INTERNAL;
} }
} }
+305
View File
@@ -0,0 +1,305 @@
// 脾神(hj_transform):Markdown → canonical ContentBlock 确定性解析器。
//
// v1 支持范围(parser_version = hunjing-markdown-v1):
// - fenced 代码块(```lang)→ code block;```mermaid → mermaid block;
// - 独占行的 $$ … $$(闭合)→ math block(display=block);
// - 未闭合的 fenced 或数学定界保持源码态,归入 markdown block;
// - 其余内容合并为 markdown block(前后空白裁剪)。
// 行内 $...$ 数学与完整方言解析随 WP-03 迭代(版本号同步递增)。
//
// 全部输入按不可信处理:长度与结构硬限额、UTF-8 校验;输出 JSON 键序确定。
#include "hunjing/lingtai.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
#include "engine_internal.h"
namespace {
constexpr size_t kMaxInputBytes = 1 << 20; // 单次输入上限 1 MiB
constexpr size_t kMaxBlocks = 256; // 单次产出的 block 数上限
constexpr size_t kMaxLineBytes = 1 << 16; // 单行上限 64 KiB
// —— UTF-8 校验(拒绝截断与非法序列)——
bool valid_utf8(std::string_view s) {
size_t i = 0;
const size_t n = s.size();
while (i < n) {
const unsigned char c = static_cast<unsigned char>(s[i]);
if (c < 0x80) {
++i;
continue;
}
size_t len = 0;
unsigned int min = 0;
if ((c & 0xE0) == 0xC0) { len = 2; min = 0x80; }
else if ((c & 0xF0) == 0xE0) { len = 3; min = 0x800; }
else if ((c & 0xF8) == 0xF0) { len = 4; min = 0x10000; }
else { return false; }
if (i + len > n) { return false; }
unsigned int cp = c & (0x7F >> len);
for (size_t k = 1; k < len; ++k) {
const unsigned char cc = static_cast<unsigned char>(s[i + k]);
if ((cc & 0xC0) != 0x80) { return false; }
cp = (cp << 6) | (cc & 0x3F);
}
if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { return false; }
i += len;
}
return true;
}
// —— 确定性 JSON 输出 ——
void json_escape_append(std::string &out, std::string_view value) {
out.push_back('"');
for (char ch : value) {
const unsigned char c = static_cast<unsigned char>(ch);
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
out += buf;
} else {
out.push_back(ch);
}
}
}
out.push_back('"');
}
std::string trim(std::string_view s) {
size_t b = 0, e = s.size();
while (b < e && (s[b] == ' ' || s[b] == '\t' || s[b] == '\r' || s[b] == '\n')) ++b;
while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t' || s[e - 1] == '\r' || s[e - 1] == '\n')) --e;
return std::string(s.substr(b, e - b));
}
bool starts_with_fence(std::string_view line, std::string *lang) {
size_t i = 0;
while (i < line.size() && line[i] == '`') ++i;
if (i < 3) return false;
*lang = trim(line.substr(i));
return true;
}
bool is_fence_end(std::string_view line) {
size_t i = 0;
while (i < line.size() && line[i] == '`') ++i;
return i >= 3 && trim(std::string_view(line).substr(i)).empty();
}
bool is_block_math_delim(std::string_view line) { return trim(line) == "$$"; }
struct Block {
std::string type; // markdown / code / mermaid / math
std::string text; // markdown 文本
std::string source; // code/mermaid/math 源码
std::string language; // code 语言(可空)
bool display_block = true; // math:block/inline(v1 仅 block)
};
void append_block_json(std::string &out, const Block &b, bool *first) {
if (!*first) out.push_back(',');
*first = false;
out += "{\"type\":";
json_escape_append(out, b.type);
if (b.type == "markdown") {
out += ",\"text\":";
json_escape_append(out, b.text);
out += ",\"flavor\":\"hunjing-gfm-v1\"";
} else if (b.type == "code") {
out += ",\"source\":";
json_escape_append(out, b.source);
if (!b.language.empty()) {
out += ",\"language\":";
json_escape_append(out, b.language);
}
} else if (b.type == "mermaid") {
out += ",\"source\":";
json_escape_append(out, b.source);
} else { // math
out += ",\"notation\":\"latex\",\"display\":\"block\",\"source\":";
json_escape_append(out, b.source);
}
out += ",\"status\":\"completed\"}";
}
// 主解析:逐行扫描。未闭合结构整体保持源码态(契约第 4 节)。
bool parse_markdown(std::string_view input, std::string *error, std::string *out_json) {
std::vector<std::string> lines;
{
size_t start = 0;
while (start <= input.size()) {
size_t nl = input.find('\n', start);
if (nl == std::string_view::npos) {
if (start < input.size()) lines.emplace_back(input.substr(start));
break;
}
lines.emplace_back(input.substr(start, nl - start));
start = nl + 1;
}
}
for (const auto &line : lines) {
if (line.size() > kMaxLineBytes) {
*error = "单行超过长度上限";
return false;
}
}
std::vector<Block> blocks;
std::string text_acc;
auto flush_text = [&]() {
const std::string t = trim(text_acc);
if (!t.empty()) {
blocks.push_back(Block{.type = "markdown", .text = t});
}
text_acc.clear();
};
size_t i = 0;
const size_t n = lines.size();
while (i < n) {
std::string lang;
if (starts_with_fence(lines[i], &lang)) {
std::string source;
size_t j = i + 1;
bool closed = false;
while (j < n) {
if (is_fence_end(lines[j])) { closed = true; break; }
if (!source.empty()) source.push_back('\n');
source += lines[j];
++j;
}
if (!closed) {
// 未闭合:整段保持源码态并入 markdown 文本
if (!text_acc.empty()) text_acc.push_back('\n');
text_acc += lines[i];
for (size_t k = i + 1; k < n; ++k) {
text_acc.push_back('\n');
text_acc += lines[k];
}
i = n;
break;
}
flush_text();
if (lang == "mermaid") {
blocks.push_back(Block{.type = "mermaid", .source = source});
} else {
Block b{.type = "code", .source = source, .language = lang};
blocks.push_back(b);
}
i = j + 1;
continue;
}
if (is_block_math_delim(lines[i])) {
std::string source;
size_t j = i + 1;
bool closed = false;
while (j < n) {
if (is_block_math_delim(lines[j])) { closed = true; break; }
if (!source.empty()) source.push_back('\n');
source += lines[j];
++j;
}
if (!closed) {
if (!text_acc.empty()) text_acc.push_back('\n');
text_acc += lines[i];
for (size_t k = i + 1; k < n; ++k) {
text_acc.push_back('\n');
text_acc += lines[k];
}
i = n;
break;
}
flush_text();
blocks.push_back(Block{.type = "math", .source = trim(source)});
i = j + 1;
continue;
}
if (!text_acc.empty()) text_acc.push_back('\n');
text_acc += lines[i];
++i;
}
flush_text();
if (blocks.size() > kMaxBlocks) {
*error = "block 数超过上限";
return false;
}
out_json->clear();
out_json->append("{\"parser_version\":\"");
out_json->append(hj_transform_version());
out_json->append("\",\"blocks\":[");
bool first = true;
for (const auto &b : blocks) {
append_block_json(*out_json, b, &first);
}
out_json->append("]}");
return true;
}
} // namespace
extern "C" {
const char *hj_transform_version(void) { return "hunjing-markdown-v1"; }
HjStatus hj_transform_markdown(HjEngine *engine, const char *input, size_t input_len,
HjBuffer **out) {
try {
hj_internal_set_error("");
if (out == nullptr) {
return HJ_ERR_INVALID_ARGUMENT;
}
*out = nullptr;
if (!hj_internal_engine_valid(engine)) {
hj_internal_set_error("engine 句柄不在登记表");
return HJ_ERR_HANDLE_INVALID;
}
if (input == nullptr && input_len > 0) {
hj_internal_set_error("input 为空但长度非零");
return HJ_ERR_INVALID_ARGUMENT;
}
if (input_len > kMaxInputBytes) {
hj_internal_set_error("输入超过长度上限");
return HJ_ERR_LIMIT_EXCEEDED;
}
const std::string_view view(input == nullptr ? "" : input, input_len);
if (!valid_utf8(view)) {
hj_internal_set_error("输入不是合法 UTF-8");
return HJ_ERR_INVALID_ARGUMENT;
}
std::string error;
std::string json;
if (!parse_markdown(view, &error, &json)) {
hj_internal_set_error(error.c_str());
return error.find("上限") != std::string::npos ? HJ_ERR_LIMIT_EXCEEDED
: HJ_ERR_PARSE_FAILED;
}
HjBuffer *buf = hj_internal_buffer_new(json);
if (buf == nullptr) {
hj_internal_set_error("输出缓冲分配失败");
return HJ_ERR_OUT_OF_MEMORY;
}
*out = buf;
return HJ_OK;
} catch (...) {
hj_internal_set_error("引擎内部错误");
return HJ_ERR_INTERNAL;
}
}
} // extern "C"
-1
View File
@@ -1,5 +1,4 @@
// 灵台引擎核心测试(doctest,ADR-025)。 // 灵台引擎核心测试(doctest,ADR-025)。
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "hunjing/lingtai.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);
}
}
}