0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(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%。
1430 lines
50 KiB
C
1430 lines
50 KiB
C
/*
|
||
* 模块名称: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;
|
||
}
|