✨ 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

+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 边界。
#include "hunjing/lingtai.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_set>
// 不透明句柄的实际布局(不进入契约;仅本翻译单元可见字段)。
#include "engine_internal.h"
// 不透明句柄的实际布局(仅本翻译单元可见字段)。
struct HjEngine {
uint32_t abi_major;
};
struct HjBuffer {
size_t len;
};
namespace {
// 线程局部错误消息(脱敏、UTF-8);下一次引擎调用覆盖。
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::unordered_set<HjEngine *> g_handle_table;
} // 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" {
uint32_t hj_abi_version(void) { return HJ_ABI_VERSION; }
HjStatus hj_engine_new(uint32_t required_abi_major, HjEngine **out_engine) {
try {
set_last_error("");
g_last_error.clear();
if (out_engine == nullptr) {
set_last_error("out_engine 不能为空");
g_last_error = "out_engine 不能为空";
return HJ_ERR_INVALID_ARGUMENT;
}
*out_engine = nullptr;
if (required_abi_major != HJ_ABI_VERSION) {
set_last_error("ABI major 不匹配:拒绝初始化,不静默降级");
g_last_error = "ABI major 不匹配:拒绝初始化,不静默降级";
return HJ_ERR_UNSUPPORTED_ABI;
}
auto *engine = new (std::nothrow) HjEngine{required_abi_major};
if (engine == nullptr) {
set_last_error("引擎内存分配失败");
g_last_error = "引擎内存分配失败";
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;
} catch (...) {
// 入口捕获一切异常并转换为 HjStatus(契约禁令)。
set_last_error("引擎内部错误");
g_last_error = "引擎内部错误";
return HJ_ERR_INTERNAL;
}
}
HjStatus hj_engine_free(HjEngine *engine) {
try {
set_last_error("");
g_last_error.clear();
{
const std::lock_guard<std::mutex> lock(g_handle_table_mutex);
if (g_handle_table.erase(engine) == 0) {
set_last_error("句柄不在登记表:已释放或非法");
g_last_error = "句柄不在登记表:已释放或非法";
return HJ_ERR_HANDLE_INVALID;
}
}
delete engine;
return HJ_OK;
} catch (...) {
set_last_error("引擎内部错误");
g_last_error = "引擎内部错误";
return HJ_ERR_INTERNAL;
}
}
HjStatus hj_buffer_free(HjBuffer *buffer) {
try {
set_last_error("");
g_last_error.clear();
if (buffer == nullptr) {
set_last_error("buffer 句柄为空");
g_last_error = "buffer 句柄为空";
return HJ_ERR_INVALID_ARGUMENT;
}
free(buffer);
return HJ_OK;
} catch (...) {
set_last_error("引擎内部错误");
g_last_error = "引擎内部错误";
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"