Files
STM32F4-Base/App/cfg/cfg_file.c
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +08:00

1430 lines
50 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.
/*
* 模块名称:cfg_file
* 模块功能:通用配置文件引擎(双端共享核心)——INI 段/键表解析校验、外科手术式
* 保存(保留注释/未知内容,缺失段/键写回)、出厂模板生成、沉降去抖轮询
* 与分段变更回调;平台文件操作经 cfg_file_store 后端
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-14
* 修改记录:
* v1.0 2026-09-14 王建锋 创建初始版本(P3:段键表引擎 + net_cfg 集成)
* v1.1 2026-09-18 王建锋 临时缓冲改经 cfg_store_alloc(MCU = FreeRTOS 堆),
* 移除对 C 库 malloc 的全部依赖(MicroLIB C 堆仅 512B,
* 原 4 处 KB 级 malloc 真机必然失败);增编译期堆峰值预算
* 校验 + 失败上报自检记录
* v1.2 2026-09-21 王建锋 超长文件守卫根治——poll 判定
* 前置到 cfg_store_stat 真实 st.size(旧检查位于截断后的
* len 上恒假=死码,超长文件被截断当有效解析 → 界外静默
* 回工厂默认 → 基线粘住 → 用户配置销毁链);save 输出上限
* 由 CFG_OUT_MAX 收紧至 CFG_FILE_MAX_LEN;s_write_doc 写盘
* 唯一闸口拒写超长文档;load/poll 超长处置补自检留痕
* v1.3 2026-10-04 王建锋 受管键行超 CFG_LINE_TEST_MAX 时拒绝保存——
* 原实现 Pass1 静默跳过超长行,Pass2 遂在段头补写新 KV 且
* 原文超长行原样保留 → 同一键出现两行(旧值残留、静默截断)
* v1.4 2026-10-15 王建锋 生成文档边界余量收口——
* s_gen_document 两处 `off += snprintf(...)` 改检查式(截断即
* 返回 -1,不再跳过缓冲末尾);注册拒超长键名(≤ 15 字符),
* 使每行 112 字节余量成为可编译期验证的前提
* v1.5 2026-10-25 DTU 项目组 数据可信卡 3:新增值类型 CFG_VAL_INT(有符号十进制,
* len 1/2/4 小端补码)——s_decode_int() 用 strtol + ERANGE +
* 宽度上下界校验,s_encode() 带负号输出;负零点偏移从此可
* 干净落盘(动因见 docs/任务规划/数据可信.md §4.4 与 §7 决策 3)
* 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save;
* 启动期 load 先于任务竞争);FS 卷并发由 FatFS REENTRANT 卷锁保证
* (见《配置文件设计说明》D15 A+ 方案)
*/
/* 头文件包含区 */
#include <string.h>
#include <stdlib.h> /* strtoul / strtol(键值解码);堆分配一律走 cfg_store_alloc,禁 malloc */
#include <stdio.h>
#include <ctype.h>
#include <errno.h> /* ERANGE:strtol 饱和判别(INT 键解码需要,数据可信卡 3) */
#include "cfg_file.h"
#include "cfg_file_store.h"
#include "sys_log.h" /* 配置读写失败上报自检记录(双端共享接口,MCU 端 preboot/队列安全) */
/* 深调用链模块(配置引擎在 defaultTask 启动链与保存链上,下接 FatFS → FTL):
* 日志行缓冲收到 128 B(默认 192)。本模块最长行 "load: file %luB > %uB, corrupt,
* factory template regenerated" 渲染后 ≈ 91 B(含时间戳 21 + 标签)< 128 → 不截断 */
#define DBG_LINE_BUF_MAX 128U
#define DBG_TAG "[CFG]"
#include "dbg_log.h"
/* 常量定义区 */
/* 文件头注释块(首次生成文件时写入;后续保存逐行保留) */
static const char s_doc_header[] =
"; ============================================================\r\n"
"; STM32F4-Base 通用配置文件\r\n"
"; 格式: INI (UTF-8 / CRLF),本文件为配置唯一来源\r\n"
"; - 以 ';' 或 '#' 开头的行为注释,保存时保留\r\n"
"; - 未知段/键保存时保留;缺失段/键保存时写回\r\n"
"; - 编辑后经 FTP 上传或设备端保存,设备 4s 沉降后自动重载\r\n"
"; ============================================================\r\n";
#define CFG_OUT_MAX 5120u /* 保存输出缓冲上限(经 cfg_store_alloc 取,见下方堆峰值预算) */
#define CFG_LINE_TEST_MAX 127u /* 受管键行匹配的行长上限(超长必非受管键行) */
#define CFG_SNAP_MAX 64u /* 变更检测段快照缓冲(挂回调段 data_size 须 ≤ 此值) */
#define CFG_NAME_MAX (CFG_FILE_MAX_NAME_LEN + 1u) /* 段名/键名缓冲(15 字符 + NUL) */
#define CFG_HEAP_BUDGET 10240u /* 暂态堆峰值预算:读缓冲(1B NUL) + 输出缓冲之和上限 */
/* 编译期钉死暂态堆峰值:放大 CFG_FILE_MAX_LEN / CFG_OUT_MAX 会先在此编译失败,强制
* 改动者重新核对平台堆余量(MCU = FreeRTOS 堆,当前实测余量见《RAM使用分析与优化方案》),
* 而不是在真机上退化成运行期分配失败。用 typedef 负数组实现(ARMCC 5 C99 无 C11
* _Static_assert);当前峰值 4097 + 5120 = 9217B ≤ 10240B */
typedef char cfg_heap_peak_check_t[(CFG_FILE_MAX_LEN + 1u + CFG_OUT_MAX) <= CFG_HEAP_BUDGET ? 1 : -1];
/* 静态变量定义区 */
typedef struct {
char name[CFG_NAME_MAX];
void *p_data;
const void *p_defaults;
size_t data_size;
const cfg_key_def_t *p_keys;
uint8_t key_count;
void (*p_cb)(void);
uint8_t used;
} cfg_sec_t;
static cfg_sec_t s_sections[CFG_FILE_MAX_SECTIONS];
static cfg_file_stat_t s_base; /* 变更基线 (size, mtime 令牌) */
static cfg_file_stat_t s_last; /* 上次 poll 的 stat */
static uint8_t s_stable; /* 连续相同轮询计数(沉降去抖) */
static uint8_t s_base_valid; /* 基线是否已建立 */
static uint8_t s_snap[CFG_SNAP_MAX];
static int s_sec_hdr[CFG_FILE_MAX_SECTIONS]; /* 段头行号(0xFFFF = 未见) */
static uint16_t s_key_line[CFG_FILE_MAX_SECTIONS][CFG_FILE_MAX_KEYS]; /* 受管键行号(0xFFFF = 未见) */
/* 函数定义区 */
/*
* 函数功能:短字符串大小写无关比较
* 入口参数:p_a / p_b - 两个 NUL 结尾字符串 const char *
* 返回值:1 - 相等(忽略大小写);0 - 不等 int
* 限定条件:p_a/p_b 非空
*/
static int s_str_ci_eq(const char *p_a, const char *p_b) {
for (;;) {
char ca = *p_a;
char cb = *p_b;
if (ca >= 'A' && ca <= 'Z') {
ca = (char)(ca + ('a' - 'A'));
}
if (cb >= 'A' && cb <= 'Z') {
cb = (char)(cb + ('a' - 'A'));
}
if (ca != cb) {
return 0;
}
if (ca == '\0') {
return 1;
}
p_a++;
p_b++;
}
}
/*
* 函数功能:取下一行(行末 NUL 化、剥 CR、前导空白左移)并推进缓冲位置
* 入口参数:p_p - 当前位置指针(入/出) char **
* 返回值:行首指针 char *
* 限定条件:*p_p 指向可写缓冲内
* 函数说明:行界定 = '\n'(缺失时到 NUL);会修改缓冲(行末字节,
* 含 CRLF 的 '\r'),保存路径 Pass 1 须用一次性副本
*/
static char *s_split_line(char **p_p) {
char *p_line = *p_p;
char *p_nl;
char *p_ws;
size_t l;
p_nl = strchr(p_line, '\n');
if (p_nl != NULL) {
*p_nl = '\0';
*p_p = p_nl + 1;
}
else {
*p_p = p_line + strlen(p_line) + 1;
}
l = strlen(p_line);
if (l > 0u && p_line[l - 1u] == '\r') {
p_line[l - 1u] = '\0';
}
p_ws = p_line;
while (*p_ws == ' ' || *p_ws == '\t') {
p_ws++;
}
if (p_ws != p_line) {
(void)memmove(p_line, p_ws, strlen(p_ws) + 1u);
}
return p_line;
}
/*
* 函数功能:识别段名行(非修改式)
* 入口参数:p_line - 行文本 const char *
* p_name - 段名输出缓冲 char *
* name_cap- 缓冲容量 size_t
* 返回值:1 - 是段名行(p_name 已填);0 - 不是 int
* 限定条件:p_line/p_name 非空
*/
static int s_match_section(const char *p_line, char *p_name, size_t name_cap) {
const char *p;
const char *p_br;
size_t n = 0u;
if (p_line[0] != '[') {
return 0;
}
p_br = strchr(p_line, ']');
if (p_br == NULL || (size_t)(p_br - p_line) >= name_cap) {
return 0;
}
p = p_line + 1;
while (p != p_br && (n + 1u) < name_cap) {
p_name[n++] = (char)*p;
p++;
}
p_name[n] = '\0';
while (n > 0u && (p_name[n - 1u] == ' ' || p_name[n - 1u] == '\t')) {
p_name[--n] = '\0';
}
return 1;
}
/*
* 函数功能:拆分键值行(修改式:键在 '=' 处 NUL 化,值区空白修剪)
* 入口参数:p_line - 行文本(可写副本) char *
* p_key - 键名输出(指向 p_line 内) char **
* 返回值:值文本指针;NULL - 不是键值行 char *
* 限定条件:仅用于一次性解析(缓冲可被修改)
*/
static char *s_key_value(char *p_line, char **p_key) {
char *p_eq = strchr(p_line, '=');
char *p_val;
size_t kl;
size_t vl;
if (p_eq == NULL) {
return NULL;
}
*p_eq = '\0';
kl = (size_t)(p_eq - p_line);
while (kl > 0u && (p_line[kl - 1u] == ' ' || p_line[kl - 1u] == '\t')) {
p_line[--kl] = '\0';
}
*p_key = p_line;
p_val = p_eq + 1;
{
char *p_ws = p_val;
while (*p_ws == ' ' || *p_ws == '\t') {
p_ws++;
}
if (p_ws != p_val) {
(void)memmove(p_val, p_ws, strlen(p_ws) + 1u);
}
}
vl = strlen(p_val);
while (vl > 0u && (p_val[vl - 1u] == ' ' || p_val[vl - 1u] == '\t')) {
p_val[--vl] = '\0';
}
return p_val;
}
/*
* 函数功能:RAM 字段编码为 INI 值文本
* 入口参数:p_key - 键定义 const cfg_key_def_t *
* p_src - RAM 字段 const uint8_t *
* p_dst - 输出缓冲 char *
* cap - 缓冲容量 size_t
* 返回值:成功 = 文本长度;失败 = -1(超长/非法字段长度) int
* 限定条件:p_src/p_dst 非空
*/
static int s_encode(const cfg_key_def_t *p_key, const uint8_t *p_src, char *p_dst, size_t cap) {
int n;
switch (p_key->type) {
case CFG_VAL_UINT: {
unsigned long v = 0uL;
size_t i;
if (p_key->len != 1u && p_key->len != 2u && p_key->len != 4u) {
return -1;
}
for (i = 0u; i < p_key->len; i++) {
v |= (unsigned long)p_src[i] << (8u * i);
}
n = snprintf(p_dst, cap, "%lu", v);
break;
}
case CFG_VAL_INT: {
long v = 0L;
unsigned long raw = 0uL;
size_t i;
if (p_key->len != 1u && p_key->len != 2u && p_key->len != 4u) {
return -1;
}
for (i = 0u; i < p_key->len; i++) {
raw |= ((unsigned long)p_src[i]) << (8u * i);
}
/* 小端补码还原为有符号值(两端 long = 32 位:ARMCC 32 位 ARM / MSVC LLP64) */
if (p_key->len == 1u) {
v = (long)(int8_t)(raw & 0xFFuL);
} else if (p_key->len == 2u) {
v = (long)(int16_t)(raw & 0xFFFFuL);
} else {
v = (long)(int32_t)raw;
}
n = snprintf(p_dst, cap, "%ld", v);
break;
}
case CFG_VAL_IP4:
n = snprintf(p_dst, cap, "%u.%u.%u.%u",
(unsigned)p_src[0], (unsigned)p_src[1],
(unsigned)p_src[2], (unsigned)p_src[3]);
break;
case CFG_VAL_MAC:
n = snprintf(p_dst, cap, "%02x:%02x:%02x:%02x:%02x:%02x",
(unsigned)p_src[0], (unsigned)p_src[1], (unsigned)p_src[2],
(unsigned)p_src[3], (unsigned)p_src[4], (unsigned)p_src[5]);
break;
case CFG_VAL_STR: {
size_t slen;
/* len==0 时 `p_key->len - 1u` 回绕为 SIZE_MAX → memcpy 巨长越界。
* STR 字段至少 1 字节(含结尾 NUL),len==0 非法,直接拒绝 */
if (p_key->len == 0u) {
return -1;
}
slen = strlen((const char *)p_src);
if (slen >= p_key->len) {
slen = p_key->len - 1u;
}
(void)memcpy(p_dst, p_src, slen);
p_dst[slen] = '\0';
n = (int)slen;
break;
}
default:
return -1;
}
if (n < 0 || (size_t)n >= cap) {
return -1;
}
return n;
}
/*
* 函数功能:解析无符号十进制值(小端写入)
* 入口参数:p_dst - 目标字段 uint8_t *
* len - 字段字节数(1/2/4) size_t
* p_val - 值文本 const char *
* 返回值:0 - 成功;-1 - 非法(空/非数字/超范围/带后缀) int
* 限定条件:无
*/
static int s_decode_uint(uint8_t *p_dst, size_t len, const char *p_val) {
const unsigned long max_v = (len == 1u) ? 0xFFu : ((len == 2u) ? 0xFFFFu : 0xFFFFFFFFuL);
char *p_end = NULL;
unsigned long v;
size_t i;
if (len != 1u && len != 2u && len != 4u) {
return -1;
}
if (p_val[0] == '\0') {
return -1;
}
v = strtoul(p_val, &p_end, 10);
if (p_end == p_val || *p_end != '\0' || v > max_v) {
return -1;
}
for (i = 0u; i < len; i++) {
p_dst[i] = (uint8_t)(v >> (8u * i));
}
return 0;
}
/*
* 函数功能:解析有符号十进制值(小端补码写入,数据可信卡 3)
* 入口参数:p_dst - 目标字段 uint8_t *
* len - 字段字节数(1/2/4) size_t
* p_val - 值文本(可带 +/- 号) const char *
* 返回值:0 - 成功;-1 - 非法(空/非数字/带后缀/超本宽度范围/strtol 饱和) int
* 限定条件:无
* 函数说明:两端 long = 32 位,strtol 对超范围文本饱和到 LONG_MAX/MIN 并置
* ERANGE(如 "2147483648" 饱和成 INT32_MAX 本身,光比 v>max_v 抓不住),
* 故 ERANGE 与宽度上下界两道都要查;1/2 字节键按 int8/int16 范围收窄
*/
static int s_decode_int(uint8_t *p_dst, size_t len, const char *p_val) {
const long min_v = (len == 1u) ? -128L : ((len == 2u) ? -32768L : (-2147483647L - 1L));
const long max_v = (len == 1u) ? 127L : ((len == 2u) ? 32767L : 2147483647L);
char *p_end = NULL;
long v;
size_t i;
if (len != 1u && len != 2u && len != 4u) {
return -1;
}
if (p_val[0] == '\0') {
return -1;
}
errno = 0;
v = strtol(p_val, &p_end, 10);
if (p_end == p_val || *p_end != '\0' || errno == ERANGE) {
return -1;
}
if (v < min_v || v > max_v) {
return -1;
}
for (i = 0u; i < len; i++) {
p_dst[i] = (uint8_t)(((unsigned long)v) >> (8u * i));
}
return 0;
}
/*
* 函数功能:解析点分 IPv4("a.b.c.d",每段 0~255)
* 入口参数:p_dst - 4 字节目标 uint8_t *
* p_val - 值文本 const char *
* 返回值:0 - 成功;-1 - 非法 int
* 限定条件:无
*/
static int s_decode_ip4(uint8_t *p_dst, const char *p_val) {
int i;
const char *p = p_val;
for (i = 0; i < 4; i++) {
unsigned v = 0u;
int nd = 0;
if (i != 0) {
if (*p != '.') {
return -1;
}
p++;
}
if (!isdigit((unsigned char)*p)) {
return -1;
}
while (isdigit((unsigned char)*p) && nd < 3) {
v = (unsigned)(v * 10u + (unsigned)(*p - '0'));
p++;
nd++;
}
if (v > 255u) {
return -1;
}
p_dst[i] = (uint8_t)v;
}
return (*p == '\0') ? 0 : -1;
}
/*
* 函数功能:解析十六进制 MAC(分隔符兼容 ':' / '-' / '.',大小写均可)
* 入口参数:p_dst - 6 字节目标 uint8_t *
* p_val - 值文本 const char *
* 返回值:0 - 成功;-1 - 非法 int
* 限定条件:无
*/
static int s_decode_mac(uint8_t *p_dst, const char *p_val) {
int i;
const char *p = p_val;
for (i = 0; i < 12; i++) {
int v;
if ((i & 1) == 0 && i != 0) {
if (*p != ':' && *p != '-' && *p != '.') {
return -1;
}
p++;
}
if (*p >= '0' && *p <= '9') {
v = *p - '0';
}
else if (*p >= 'a' && *p <= 'f') {
v = *p - 'a' + 10;
}
else if (*p >= 'A' && *p <= 'F') {
v = *p - 'A' + 10;
}
else {
return -1;
}
if ((i & 1) == 0) {
p_dst[i >> 1] = (uint8_t)v;
}
else {
p_dst[i >> 1] = (uint8_t)((p_dst[i >> 1] << 4) | (uint8_t)v);
}
p++;
}
return (*p == '\0') ? 0 : -1;
}
/*
* 函数功能:解析字符串值(NUL 结尾写入)
* 入口参数:p_dst - 目标缓冲 uint8_t *
* len - 缓冲长度 size_t
* p_val - 值文本 const char *
* 返回值:0 - 成功;-1 - 超长(含 NUL > len) int
* 限定条件:无
*/
static int s_decode_str(uint8_t *p_dst, size_t len, const char *p_val) {
size_t vlen = strlen(p_val);
if (len < vlen + 1u) {
return -1;
}
(void)memcpy(p_dst, p_val, vlen);
p_dst[vlen] = '\0';
return 0;
}
/*
* 函数功能:值文本解码到 RAM 字段(按键类型分派)
* 入口参数:p_key - 键定义 const cfg_key_def_t *
* p_dst - 目标字段 uint8_t *
* p_val - 值文本 const char *
* 返回值:0 - 成功;-1 - 非法(调用方留工厂默认并留痕) int
* 限定条件:无
*/
static int s_decode(const cfg_key_def_t *p_key, uint8_t *p_dst, const char *p_val) {
switch (p_key->type) {
case CFG_VAL_UINT:
return s_decode_uint(p_dst, p_key->len, p_val);
case CFG_VAL_INT:
return s_decode_int(p_dst, p_key->len, p_val);
case CFG_VAL_IP4:
return s_decode_ip4(p_dst, p_val);
case CFG_VAL_MAC:
return s_decode_mac(p_dst, p_val);
case CFG_VAL_STR:
return s_decode_str(p_dst, p_key->len, p_val);
default:
return -1;
}
}
/*
* 函数功能:按各段 RAM 副本生成完整配置文档(文件头注释 + 全部段/键)
* 入口参数:p_buf - 输出缓冲 char *
* cap - 缓冲容量 size_t
* p_len - 输出长度 size_t *
* 返回值:0 - 成功;-1 - 缓冲不足/编码失败 int
* 限定条件:p_buf/p_len 非空;调用前各段 RAM 副本须为期望落盘值
*/
static int s_gen_document(char *p_buf, size_t cap, size_t *p_len) {
size_t off;
int n;
int i;
int k;
size_t hlen;
if (p_buf == NULL || p_len == NULL) {
return -1;
}
hlen = strlen(s_doc_header);
if (cap < hlen + 1u) {
return -1;
}
(void)memcpy(p_buf, s_doc_header, hlen + 1u);
off = hlen;
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
const cfg_sec_t *p_sec = &s_sections[i];
if (p_sec->used == 0) {
continue;
}
if (cap < off + 112u) {
return -1;
}
n = snprintf(p_buf + off, cap - off, "\r\n[%s]\r\n", p_sec->name);
if (n < 0 || (size_t)n >= cap - off) {
return -1;
}
off += (size_t)n;
for (k = 0; k < (int)p_sec->key_count; k++) {
const cfg_key_def_t *p_key = &p_sec->p_keys[k];
char v[80];
if (s_encode(p_key, (const uint8_t *)p_sec->p_data + p_key->offset, v, sizeof(v)) < 0) {
return -1;
}
if (cap < off + 112u) {
return -1;
}
n = snprintf(p_buf + off, cap - off, "%s=%s\r\n", p_key->name, v);
if (n < 0 || (size_t)n >= cap - off) {
return -1;
}
off += (size_t)n;
}
}
*p_len = off;
return 0;
}
/*
* 函数功能:读配置文件到平台堆缓冲(NUL 结尾)
* 入口参数:p_pbuf - 缓冲指针输出 uint8_t **
* p_len - 实际长度输出 size_t *
* 返回值:0 - 成功;-1 - 分配/读失败(缓冲已释放) int
* 限定条件:成功时调用方须 cfg_store_free(*p_pbuf)(MCU = FreeRTOS 堆,勿用 C 库 free)
*/
static int s_load_file(uint8_t **p_pbuf, size_t *p_len) {
uint8_t *p_buf;
size_t nread = 0u;
int r;
if (p_pbuf == NULL || p_len == NULL) {
return -1;
}
*p_pbuf = NULL;
p_buf = (uint8_t *)cfg_store_alloc(CFG_FILE_MAX_LEN + 1u);
if (p_buf == NULL) {
DBG_ERROR("read: alloc %uB FAIL (heap free %uB)",
(unsigned)(CFG_FILE_MAX_LEN + 1u), (unsigned)cfg_store_heap_free());
return -1;
}
r = cfg_store_read(cfg_store_config_path(), p_buf, CFG_FILE_MAX_LEN, &nread);
if (r != 0) {
cfg_store_free(p_buf);
return -1;
}
p_buf[nread] = '\0';
*p_pbuf = p_buf;
*p_len = nread;
return 0;
}
/*
* 函数功能:经后端原子写配置文档(写盘唯一闸口:模板/外科手术同闸)
* 入口参数:p_buf - 文档数据 const uint8_t *
* len - 数据长度 size_t
* 返回值:0 - 成功;-1 - 写失败或超长拒写 int
* 限定条件:p_buf 非空;len ≤ CFG_FILE_MAX_LEN
* 函数说明:纵深防御——超长文档一旦落盘,下次开机 load 守卫即判损坏、
* 整份用户配置连同注释/未知段销毁,故在唯一出口硬性拦截,使
* "引擎永不产出超长 config.ini"成为结构性质(调用者新增路径免疫)
*/
static int s_write_doc(const uint8_t *p_buf, size_t len) {
if (p_buf == NULL) {
return -1;
}
if (len > CFG_FILE_MAX_LEN) {
DBG_ERROR("doc: %luB > %uB limit, write rejected",
(unsigned long)len, (unsigned)CFG_FILE_MAX_LEN);
return -1;
}
return cfg_store_write_atomic(cfg_store_config_path(), p_buf, len);
}
/*
* 函数功能:按当前 RAM 副本重建出厂模板写盘
* 入口参数:无
* 返回值:0 - 成功;-1 - 失败 int
* 限定条件:各段 RAM 副本须为期望落盘值
*/
static int s_write_template(void) {
char *p_buf = (char *)cfg_store_alloc(CFG_OUT_MAX);
size_t len = 0u;
int r;
if (p_buf == NULL) {
DBG_ERROR("template: alloc %uB FAIL (heap free %uB)",
(unsigned)CFG_OUT_MAX, (unsigned)cfg_store_heap_free());
syslog_post(SYSLOG_SELFTEST, "cfg heap alloc fail");
return -1;
}
r = s_gen_document(p_buf, CFG_OUT_MAX, &len);
if (r == 0) {
r = s_write_doc((const uint8_t *)p_buf, len);
if (r != 0) {
syslog_post(SYSLOG_SELFTEST, "cfg template write fail");
}
}
cfg_store_free(p_buf);
return r;
}
/*
* 函数功能:解析 INI 文本,将有效键写入段 RAM 副本
* 入口参数:p_buf - 文件文本(可修改,NUL 结尾) char *
* only_sec - 仅解析该段下标;-1 = 全部段 int
* 返回值:无
* 限定条件:调用前对应段须已复位工厂默认;无效键 = 留工厂默认并留痕
*/
static void s_parse(char *p_buf, int only_sec) {
char *p = p_buf;
char p_name[CFG_NAME_MAX];
int sec_idx = -1;
while (*p != '\0') {
char *p_line = s_split_line(&p);
if (p_line[0] == '\0' || p_line[0] == ';' || p_line[0] == '#') {
continue;
}
if (s_match_section(p_line, p_name, sizeof(p_name)) != 0) {
int i;
sec_idx = -1;
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0 && s_str_ci_eq(p_name, s_sections[i].name) != 0) {
sec_idx = i;
break;
}
}
continue;
}
if (sec_idx < 0 || (only_sec >= 0 && sec_idx != only_sec)) {
continue;
}
{
char *p_key = NULL;
const char *p_val = s_key_value(p_line, &p_key);
if (p_val != NULL) {
const cfg_sec_t *p_sec = &s_sections[sec_idx];
int k;
for (k = 0; k < (int)p_sec->key_count; k++) {
if (s_str_ci_eq(p_key, p_sec->p_keys[k].name) != 0) {
if (s_decode(&p_sec->p_keys[k],
(uint8_t *)p_sec->p_data + p_sec->p_keys[k].offset,
p_val) != 0) {
DBG_ERROR("parse: [%s] %s value invalid, keep factory default",
p_sec->name, p_key);
}
break;
}
}
}
}
}
}
/*
* 函数功能:向输出缓冲追加一行原文(行文本 + CRLF;空行 = 分隔空行)
* 入口参数:p_out - 输出缓冲 char *
* out_cap - 缓冲容量 size_t
* p_off - 当前偏移(入/出) size_t *
* p_line - 行文本 const char *
* 返回值:0 - 成功;-1 - 溢出(偏移不变) int
* 限定条件:p_line 非空
*/
static int s_emit_line(char *p_out, size_t out_cap, size_t *p_off, const char *p_line) {
int n;
n = snprintf(p_out + *p_off, out_cap - *p_off, "%s\r\n", p_line);
if (n < 0 || (size_t)n >= out_cap - *p_off) {
return -1;
}
*p_off += (size_t)n;
return 0;
}
/*
* 函数功能:向输出缓冲追加"键=值"行
* 入口参数:p_out - 输出缓冲 char *
* out_cap - 缓冲容量 size_t
* p_off - 当前偏移(入/出) size_t *
* p_key - 键名 const char *
* p_val - 值文本 const char *
* 返回值:0 - 成功;-1 - 溢出(偏移不变) int
* 限定条件:p_key/p_val 非空
*/
static int s_emit_kv(char *p_out, size_t out_cap, size_t *p_off, const char *p_key, const char *p_val) {
int n;
n = snprintf(p_out + *p_off, out_cap - *p_off, "%s=%s\r\n", p_key, p_val);
if (n < 0 || (size_t)n >= out_cap - *p_off) {
return -1;
}
*p_off += (size_t)n;
return 0;
}
/*
* 函数功能:向输出缓冲追加段头行
* 入口参数:p_out - 输出缓冲 char *
* out_cap - 缓冲容量 size_t
* p_off - 当前偏移(入/出) size_t *
* p_name - 段名 const char *
* 返回值:0 - 成功;-1 - 溢出(偏移不变) int
* 限定条件:p_name 非空
*/
static int s_emit_header(char *p_out, size_t out_cap, size_t *p_off, const char *p_name) {
int n;
n = snprintf(p_out + *p_off, out_cap - *p_off, "[%s]\r\n", p_name);
if (n < 0 || (size_t)n >= out_cap - *p_off) {
return -1;
}
*p_off += (size_t)n;
return 0;
}
/*
* 函数功能:外科手术式保存变换(保留注释/未知内容,受管段/键按 RAM 副本改写)
* 入口参数:p_in - 原文本(行末已 NUL 化的可修改缓冲) char *
* in_len - 原文长度 size_t
* p_out - 输出缓冲 char *
* out_cap - 输出容量 size_t
* p_out_len - 输出长度 size_t *
* 返回值:0 - 成功;-1 - 溢出(调用方放弃保存,原文件不动) int
* 限定条件:调用前 s_sec_hdr/s_key_line 已由 Pass 1 填充
*/
static int s_surgical(char *p_in, size_t in_len, char *p_out, size_t out_cap, size_t *p_out_len) {
size_t off = 0u;
(void)in_len;
/* ---- Pass 2:逐行输出 ---- */
{
char *p = p_in;
int i_sec = -1;
int line_no = 0;
int i;
int k;
char p_name[CFG_NAME_MAX];
while (*p != '\0') {
const char *p_line = s_split_line(&p);
if (s_match_section(p_line, p_name, sizeof(p_name)) != 0) {
int idx = -1;
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0 && s_str_ci_eq(p_name, s_sections[i].name) != 0) {
idx = i;
break;
}
}
if (idx >= 0) {
if (s_emit_header(p_out, out_cap, &off, s_sections[idx].name) != 0) {
return -1;
}
for (k = 0; k < (int)s_sections[idx].key_count; k++) {
if (s_key_line[idx][k] == 0xFFFFu) {
const cfg_key_def_t *p_key = &s_sections[idx].p_keys[k];
char v[80];
/* s_encode 成功返回文本长度(≥1),失败返回 -1:
* 故判成功必须 `< 0` 取反,不能写 `== 0`(恒假,
* 使补写路径沦为死码,见 2026-10-02 L0.5 修复) */
if (s_encode(p_key, (const uint8_t *)s_sections[idx].p_data + p_key->offset,
v, sizeof(v)) >= 0) {
if (s_emit_kv(p_out, out_cap, &off, p_key->name, v) != 0) {
return -1;
}
}
}
}
i_sec = idx;
}
else {
if (s_emit_line(p_out, out_cap, &off, p_line) != 0) {
return -1;
}
i_sec = -1;
}
}
else if (i_sec >= 0) {
int is_key = 0;
for (k = 0; k < (int)s_sections[i_sec].key_count; k++) {
if (s_key_line[i_sec][k] == (uint16_t)line_no) {
is_key = 1;
break;
}
}
if (is_key) {
const cfg_key_def_t *p_key = &s_sections[i_sec].p_keys[k];
char v[80];
/* s_encode 成功返回文本长度(≥1),失败返回 -1:判成功用 `>= 0`。
* 旧代码写 `== 0`(恒假)→ 受管键行永远走下面 else 原样输出 →
* 对已存在文件保存时**受管值从不改写**(静默不落盘,2026-10-02 修复) */
if (s_encode(p_key, (const uint8_t *)s_sections[i_sec].p_data + p_key->offset,
v, sizeof(v)) >= 0) {
if (s_emit_kv(p_out, out_cap, &off, p_key->name, v) != 0) {
return -1;
}
}
else {
if (s_emit_line(p_out, out_cap, &off, p_line) != 0) {
return -1;
}
}
}
else {
if (s_emit_line(p_out, out_cap, &off, p_line) != 0) {
return -1;
}
}
}
else {
if (s_emit_line(p_out, out_cap, &off, p_line) != 0) {
return -1;
}
}
line_no++;
}
/* 文件中未见的受管段 → 文末整段补写 */
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
const cfg_sec_t *p_sec = &s_sections[i];
if (p_sec->used == 0 || s_sec_hdr[i] != 0xFFFF) {
continue;
}
if (s_emit_line(p_out, out_cap, &off, "") != 0) {
return -1;
}
if (s_emit_header(p_out, out_cap, &off, p_sec->name) != 0) {
return -1;
}
for (k = 0; k < (int)p_sec->key_count; k++) {
const cfg_key_def_t *p_key = &p_sec->p_keys[k];
char v[80];
/* 同上:s_encode 成功 = 文本长度(≥1),判成功用 `>= 0` */
if (s_encode(p_key, (const uint8_t *)p_sec->p_data + p_key->offset,
v, sizeof(v)) >= 0) {
if (s_emit_kv(p_out, out_cap, &off, p_key->name, v) != 0) {
return -1;
}
}
}
}
}
*p_out_len = off;
return 0;
}
/*
* 函数功能:判断一行(非修改式)的键名是否为指定段的受管键
* 入口参数:p_line - 行文本(只读) const char *
* i_sec - 段索引 int
* 返回值:1 - 是受管键;0 - 不是(无 '=' 或键名不匹配) int
* 限定条件:仅比较 '=' 前的键名(大小写无关、剥尾部空白),不解析值
* 函数说明:用于"超长行是否落在受管键上"的判定(值可能很长,无法拷入定长缓冲);
* 非修改式,故可安全用于仍供 Pass2 原样保留的行
*/
static int s_line_key_managed(const char *p_line, int i_sec) {
const char *p_eq = strchr(p_line, '=');
size_t kl;
int k;
if (p_eq == NULL) {
return 0;
}
kl = (size_t)(p_eq - p_line);
while (kl > 0u && (p_line[kl - 1u] == ' ' || p_line[kl - 1u] == '\t')) {
kl--;
}
for (k = 0; k < (int)s_sections[i_sec].key_count; k++) {
const char *p_name = s_sections[i_sec].p_keys[k].name;
size_t i = 0u;
int eq = 1;
while (i < kl) {
char ca = p_line[i];
char cb = p_name[i];
if (ca >= 'A' && ca <= 'Z') {
ca = (char)(ca + ('a' - 'A'));
}
if (cb >= 'A' && cb <= 'Z') {
cb = (char)(cb + ('a' - 'A'));
}
if (cb == '\0' || ca != cb) {
eq = 0;
break;
}
i++;
}
if (eq != 0 && p_name[i] == '\0') {
return 1;
}
}
return 0;
}
/*
* 函数功能:保存 Pass 1——定位段头行号与各受管键行号
* 入口参数:p_in - 原文本(可修改) char *
* 返回值:0 - 成功;-1 - 受管键行超 CFG_LINE_TEST_MAX(拒绝保存) int
* 限定条件:行末被 NUL 化且不复原——调用方须传一次性副本
* 函数说明:受管键行超过 CFG_LINE_TEST_MAX 时**不能**静默跳过
* ——Pass2 会因该行未登记而在段头补写新 KV,同时原文超长行原样保留,导致
* 同一键出现两行(旧值残留 + 新值,解析取谁取决于行序)。故此处显式拒绝:
* 返回 -1,调用方中止保存、原文件不动、自检留痕。非受管键的超长行照旧跳过
* (属未知内容,Pass2 原样保留,无冲突)。
*/
static int s_surgical_pass1(char *p_in) {
char *p = p_in;
int line_no = 0;
int i;
int k;
int i_cur = -1;
char p_name[CFG_NAME_MAX];
char lbuf[CFG_LINE_TEST_MAX];
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
s_sec_hdr[i] = 0xFFFF;
for (k = 0; k < (int)CFG_FILE_MAX_KEYS; k++) {
s_key_line[i][k] = 0xFFFFu;
}
}
while (*p != '\0') {
const char *p_line = s_split_line(&p);
if (p_line[0] == '\0' || p_line[0] == ';' || p_line[0] == '#') {
line_no++;
continue;
}
if (s_match_section(p_line, p_name, sizeof(p_name)) != 0) {
i_cur = -1;
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0 && s_str_ci_eq(p_name, s_sections[i].name) != 0) {
s_sec_hdr[i] = line_no;
i_cur = i;
break;
}
}
line_no++;
continue;
}
if (i_cur >= 0 && strlen(p_line) <= CFG_LINE_TEST_MAX) {
/* 行副本上做键名匹配(不破坏原文,供 Pass 2 原样保留) */
(void)snprintf(lbuf, sizeof(lbuf), "%s", p_line);
{
char *p_key = NULL;
const char *p_val = s_key_value(lbuf, &p_key);
if (p_val != NULL) {
for (k = 0; k < (int)s_sections[i_cur].key_count; k++) {
if (s_key_line[i_cur][k] == 0xFFFFu &&
s_str_ci_eq(p_key, s_sections[i_cur].p_keys[k].name) != 0) {
s_key_line[i_cur][k] = (uint16_t)line_no;
break;
}
}
}
}
}
else if (i_cur >= 0) {
/* 超长行(> CFG_LINE_TEST_MAX):若其键名为本段受管键 → 拒绝保存;
* 否则按未知内容放过(Pass2 原样保留) */
if (s_line_key_managed(p_line, i_cur) != 0) {
return -1;
}
}
line_no++;
}
return 0;
}
/*
* 函数功能:注册配置段
* 入口参数:p_reg - 段注册描述符(不可为 NULL) const cfg_section_reg_t *
* 返回值:0 - 成功;-1 - 参数非法/段名重复/段数超限 int
* 限定条件:启动早期调用一次
*/
int cfg_file_register(const cfg_section_reg_t *p_reg) {
int i;
int k;
int nsecs = 0;
if (p_reg == NULL || p_reg->name == NULL || p_reg->p_data == NULL ||
p_reg->p_defaults == NULL || p_reg->p_keys == NULL) {
return -1;
}
if (p_reg->key_count == 0u || p_reg->key_count > CFG_FILE_MAX_KEYS) {
return -1;
}
if (p_reg->data_size == 0u || strlen(p_reg->name) >= CFG_NAME_MAX) {
return -1;
}
if (p_reg->p_cb != NULL && p_reg->data_size > CFG_SNAP_MAX) {
return -1;
}
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0) {
if (s_str_ci_eq(s_sections[i].name, p_reg->name) != 0) {
DBG_ERROR("register: section '%s' duplicated", p_reg->name);
return -1;
}
nsecs++;
}
}
if (nsecs >= (int)CFG_FILE_MAX_SECTIONS) {
return -1;
}
for (k = 0; k < (int)p_reg->key_count; k++) {
const cfg_key_def_t *p_key = &p_reg->p_keys[k];
if (p_key->name == NULL || p_key->len == 0u ||
strlen(p_key->name) >= CFG_NAME_MAX) {
return -1;
}
if (p_key->type == CFG_VAL_UINT &&
(p_key->len != 1u && p_key->len != 2u && p_key->len != 4u)) {
return -1;
}
if (p_key->type == CFG_VAL_INT &&
(p_key->len != 1u && p_key->len != 2u && p_key->len != 4u)) {
return -1;
}
if (p_key->type == CFG_VAL_IP4 && p_key->len != 4u) {
return -1;
}
if (p_key->type == CFG_VAL_MAC && p_key->len != 6u) {
return -1;
}
if (p_key->type == CFG_VAL_STR && p_key->len > 79u) {
return -1;
}
}
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used == 0) {
cfg_sec_t *p_sec = &s_sections[i];
(void)memcpy(p_sec->name, p_reg->name, strlen(p_reg->name) + 1u);
p_sec->p_data = p_reg->p_data;
p_sec->p_defaults = p_reg->p_defaults;
p_sec->data_size = p_reg->data_size;
p_sec->p_keys = p_reg->p_keys;
p_sec->key_count = p_reg->key_count;
p_sec->p_cb = p_reg->p_cb;
p_sec->used = 1;
DBG_INFO("register: section '%s' (%u keys)", p_sec->name, (unsigned)p_reg->key_count);
return 0;
}
}
return -1;
}
/*
* 函数功能:从配置文件加载全部段
* 入口参数:无
* 返回值:0 - 正常(含回退工厂默认);-1 - 严重错误 int
* 限定条件:至少注册一个段
*/
int cfg_file_load(void) {
cfg_file_stat_t st;
int i;
int nsecs = 0;
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0) {
nsecs++;
}
}
if (nsecs == 0) {
return -1;
}
/* 各段先复位工厂默认(文件 = 配置唯一来源:缺失/损坏的键 = 工厂默认) */
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0) {
(void)memcpy(s_sections[i].p_data, s_sections[i].p_defaults, s_sections[i].data_size);
}
}
if (cfg_store_stat(cfg_store_config_path(), &st) != 0 || st.size == 0u) {
/* 文件缺失/空(首启):生成出厂模板写盘 */
if (s_write_template() != 0) {
DBG_ERROR("load: template write FAIL");
return -1;
}
DBG_INFO("load: file missing, factory template generated");
return 0;
}
if (st.size > CFG_FILE_MAX_LEN) {
/* 文件超长 = 损坏:留工厂默认 + 重建模板(补自检留痕——销毁
* 用户文件属重大事件,仅串口 DBG_ERROR 在现场无调试器时不可追溯) */
if (s_write_template() != 0) {
DBG_ERROR("load: corrupt file, template write FAIL");
return -1;
}
DBG_ERROR("load: file %luB > %uB, corrupt, factory template regenerated",
(unsigned long)st.size, (unsigned)CFG_FILE_MAX_LEN);
syslog_post(SYSLOG_SELFTEST, "cfg load oversized");
return 0;
}
{
uint8_t *p_buf = NULL;
size_t len = 0u;
if (s_load_file(&p_buf, &len) != 0) {
DBG_ERROR("load: file read FAIL, factory defaults kept");
syslog_post(SYSLOG_SELFTEST, "cfg load read fail");
return -1;
}
s_parse((char *)p_buf, -1);
cfg_store_free(p_buf);
DBG_INFO("load: OK (%luB, heap free %uB)",
(unsigned long)st.size, (unsigned)cfg_store_heap_free());
return 0;
}
}
/*
* 函数功能:保存全部段到配置文件
* 入口参数:无
* 返回值:0 - 成功;-1 - 写失败(原文件保持不动) int
* 限定条件:至少注册一个段
*/
int cfg_file_save(void) {
int i;
int nsecs = 0;
cfg_file_stat_t st;
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0) {
nsecs++;
}
}
if (nsecs == 0) {
return -1;
}
if (cfg_store_stat(cfg_store_config_path(), &st) != 0 || st.size == 0u ||
st.size > CFG_FILE_MAX_LEN) {
/* 文件缺失/空/超长 = 损坏:按当前 RAM 副本重建完整文档 */
if (s_write_template() != 0) {
DBG_ERROR("save: template write FAIL");
return -1;
}
DBG_INFO("save: OK (document regenerated)");
return 0;
}
/* 外科手术式保存:保留注释/未知段/未知键 */
{
uint8_t *p_in = NULL; /* 加载原文(pristine,供 Pass 2 重切分) */
uint8_t *p_in1 = NULL; /* Pass 1 专用副本(s_split_line 破坏性 NUL 化后丢弃) */
char *p_out = NULL;
size_t in_len = 0u;
size_t out_len = 0u;
int r;
if (s_load_file(&p_in, &in_len) != 0) {
DBG_ERROR("save: file read FAIL");
syslog_post(SYSLOG_SELFTEST, "cfg save read fail");
return -1;
}
/* Pass 1 在独立副本上切分(避免破坏原文的行结束符);
* 副本在 p_out 分配前释放,堆峰值维持 p_in + p_out(≤ CFG_HEAP_BUDGET,
* 编译期已校验,见文件常量区) */
p_in1 = (uint8_t *)cfg_store_alloc(in_len + 1u);
if (p_in1 == NULL) {
cfg_store_free(p_in);
DBG_ERROR("save: alloc %uB FAIL (heap free %uB)",
(unsigned)(in_len + 1u), (unsigned)cfg_store_heap_free());
syslog_post(SYSLOG_SELFTEST, "cfg heap alloc fail");
return -1;
}
(void)memcpy(p_in1, p_in, in_len + 1u);
if (s_surgical_pass1((char *)p_in1) != 0) {
/* 受管键行超 CFG_LINE_TEST_MAX → 中止保存(原文件不动)。此类超长行
* 只可能来自手工编辑(代码生成的受管 KV 行恒 <127B);若强行外科保存会
* 产生重复键行,故宁可拒存并留痕,也不静默写坏。 */
cfg_store_free(p_in1);
cfg_store_free(p_in);
DBG_ERROR("save: managed key line > %uB, aborted (file untouched)",
(unsigned)CFG_LINE_TEST_MAX);
syslog_post(SYSLOG_SELFTEST, "cfg save line too long");
return -1;
}
cfg_store_free(p_in1);
p_out = (char *)cfg_store_alloc(CFG_OUT_MAX);
if (p_out == NULL) {
cfg_store_free(p_in);
DBG_ERROR("save: alloc %uB FAIL (heap free %uB)",
(unsigned)CFG_OUT_MAX, (unsigned)cfg_store_heap_free());
syslog_post(SYSLOG_SELFTEST, "cfg heap alloc fail");
return -1;
}
/* 输出上限由 CFG_OUT_MAX(5120) 收紧至 CFG_FILE_MAX_LEN(4096)——
* 旧上限允许把 >4096B 文档合法写盘(= 下一颗雷:下次开机 load 守卫
* 判损坏、整份用户文件连同注释/未知段一起销毁)。超限走下方既有
* 溢出中止路径:原文件不动 + 自检上报,RAM 副本保留(可修复后重试) */
r = s_surgical((char *)p_in, in_len, p_out, CFG_FILE_MAX_LEN, &out_len);
cfg_store_free(p_in);
if (r != 0) {
cfg_store_free(p_out);
DBG_ERROR("save: output > %uB file limit, aborted (file untouched)",
(unsigned)CFG_FILE_MAX_LEN);
syslog_post(SYSLOG_SELFTEST, "cfg save overflow");
return -1;
}
r = s_write_doc((const uint8_t *)p_out, out_len);
cfg_store_free(p_out);
if (r != 0) {
DBG_ERROR("save: file write FAIL");
syslog_post(SYSLOG_SELFTEST, "cfg save write fail");
return -1;
}
DBG_INFO("save: OK (surgical, %uB -> %uB)", (unsigned)in_len, (unsigned)out_len);
return 0;
}
}
/*
* 函数功能:配置文件变更轮询(沉降去抖 + 重载 + 分段变更回调)
* 入口参数:无
* 返回值:无
* 限定条件:单一任务周期调用(2s)
*/
void cfg_file_poll(void) {
cfg_file_stat_t st;
if (cfg_store_stat(cfg_store_config_path(), &st) != 0) {
return; /* 文件缺失/读失败:保留当前 RAM 配置 */
}
if (s_base_valid == 0u) {
s_base = st;
s_last = st;
s_stable = 0u;
s_base_valid = 1u;
return;
}
if (st.size == s_last.size && st.mtime == s_last.mtime) {
if (s_stable < CFG_FILE_SETTLE_CNT) {
s_stable++;
}
}
else {
s_last = st;
s_stable = 1u;
}
if (s_stable < CFG_FILE_SETTLE_CNT ||
(st.size == s_base.size && st.mtime == s_base.mtime)) {
return; /* 未沉降或与基线一致 */
}
/* 已沉降:重载配置文件 */
{
int i;
/* 2026-09-21 审计 P0:超长守卫必须基于 cfg_store_stat 的真实
* st.size 且先于 s_load_file——s_load_file 读长钉死 CFG_FILE_MAX_LEN
* (nread ≤ 4096 恒成立),旧检查写在读后的 len 上恒假(死码):
* 超长文件被截断当有效解析 → 界外段/键静默回工厂默认 → 基线粘住 →
* 后续 save 按截断视图重写 → 界外内容永久丢失 → 下次开机 load 守卫
* 判损坏销毁整份用户文件。本分支对齐 load 契约(cfg_file.h 第 4 条):
* 超长 = 损坏 → 工厂默认 + 重建模板 + 自检留痕。
* 刻意不触发段回调:保留现有网络/在线通道(用户仍可经 FTP 修复);
* RAM=工厂 与硬件现状的分歧在下次保存/重启时收敛(重启必然收敛:
* load 读到的已是重建后的合法模板) */
if (st.size > CFG_FILE_MAX_LEN) {
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0) {
(void)memcpy(s_sections[i].p_data, s_sections[i].p_defaults,
s_sections[i].data_size);
}
}
(void)s_write_template();
DBG_ERROR("poll: file %luB > %uB, corrupt, factory template regenerated",
(unsigned long)st.size, (unsigned)CFG_FILE_MAX_LEN);
syslog_post(SYSLOG_SELFTEST, "cfg poll oversized");
}
else {
uint8_t *p_buf = NULL;
size_t len = 0u;
if (s_load_file(&p_buf, &len) != 0) {
return; /* 读失败:保留,下次 poll 重试 */
}
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
const cfg_sec_t *p_sec = &s_sections[i];
if (p_sec->used == 0) {
continue;
}
if (p_sec->p_cb != NULL) {
(void)memcpy(s_snap, p_sec->p_data, p_sec->data_size);
(void)memcpy(p_sec->p_data, p_sec->p_defaults, p_sec->data_size);
s_parse((char *)p_buf, i);
if (memcmp(s_snap, p_sec->p_data, p_sec->data_size) != 0) {
DBG_INFO("poll: section '%s' changed, reload", p_sec->name);
p_sec->p_cb();
}
}
else {
(void)memcpy(p_sec->p_data, p_sec->p_defaults, p_sec->data_size);
s_parse((char *)p_buf, i);
}
}
cfg_store_free(p_buf);
DBG_INFO("poll: config file reloaded");
}
/* 重载后刷新基线(回调触发的写盘不引发二次重载) */
{
cfg_file_stat_t st2;
if (cfg_store_stat(cfg_store_config_path(), &st2) == 0) {
s_base = st2;
}
else {
s_base = st;
}
}
}
}
/*
* 函数功能:取段 RAM 副本指针
* 入口参数:p_name - 段名 const char *
* 返回值:段 RAM 副本指针;未注册返回 NULL void *
* 限定条件:p_name 非空
*/
void *cfg_file_section_data(const char *p_name) {
int i;
if (p_name == NULL) {
return NULL;
}
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0 && s_str_ci_eq(s_sections[i].name, p_name) != 0) {
return s_sections[i].p_data;
}
}
return NULL;
}