✨ 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:
1 parent
d3c8df79a8
commit
127191c889
10 files changed
+565
-26
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user