Files
STM32F4-Base/App/cfg/cfg_file.c
T
super_agent_bot 1a2ca0467d
CI / 静态门禁 (pre-commit) (pull_request) Failing after 6m33s
CI / 主机侧单测 (MSVC) (pull_request) Successful in 35s
CI / 静态门禁 (pre-commit) (push) Failing after 5m7s
CI / 主机侧单测 (MSVC) (push) Failing after 43s
fix(cfg): 视图守卫补内容判据——同尺寸全 0 旧映射块也能拦(#73)
台架(PR #82 head d9303e05,常授权烧写验证)在三点校验版守卫上继续复现
出打回形态:卷层给出的旧映射块与目标文件恰好同长(1064B)且整块全 0,
读长 == st.size 与读前后复 stat 全部通过,解析零错误、RAM 照样打回默认
(15:21 决定性轮:盘文件 RETR 校验七段完好,重载窗口内 s_sntp 三字节段
全 0)。纯令牌层校验对"内容坏"是盲的,必须补内容判据。

处置(引擎侧,与三点校验同族):
- 新增 s_view_bad:重载/保存读到的视图若为空读、或 [0,len) 内任意偏移
  含 NUL 字节即判坏——INI UTF-8 文本约定下任何偏移都不得含 0x00;解析
  器按行切分遇 NUL 即断行,零块之后的内容本就不可达(静默截断同源故障),
  拒之不误伤任何可解析文档。不要求首字节 ';'(手写文档可直接段名起始)。
- poll:坏内容 = 与长度/令牌不符同处置——拒绝应用,RAM 与基线不动,2s
  重试自愈,首次 ERROR + 自检留痕,每 30 趟一条 WARN(限速)。
- save:坏内容同族拒存返回 -1,原文件一字节不动,调用方 dirty 保留重试。
- 边界如实保留:内容合法但陈旧的整卷视图(长度/令牌/内容三者皆一致滞后)
  引擎层无法检出,属卷层(dhara/FTL)课题,重启必然收敛;"改过配置后
  重启一次"在卷层根治前仍是最稳口径。

测试:
- TC-CONFIG-061(poll 同尺寸全 0 块 HELD + 视图刷新自愈 + 零回调)、
  TC-CONFIG-062(save 遇全 0 块拒存、文件字节/写次数不动、追平后重试
  成功);桩新增 vfs_mark_stale_zeros() 注入同长全 0 旧视图。
- cfg_file 套件 259/259;iec104 179/179、run_par 67/67、adc_cal 117/117、
  sntp 98/98、cfg_file_store_mcu 34/34 复跑全绿。
- /keil-build 0 错 0 警:Code 254,784 -> 254,820 B(+36 B);台账与同码
  凭证已同步。台架实录见 PR #82 评论:NUL 注入块实机 HELD(16:55:46)
  + WARN 节拍 30/60 趟命中(16:56:42/16:57:41)+ 视图转新鲜后重载自动
  追平;残余打回事件均为"内容合法但陈旧"整卷视图(15:19:30/16:34:59/
  16:58:38),属已声明守卫边界,盘文件全程未吞段,重启收敛实机确认
  (17:04 RAM 回文件值)。

文档:
- 《配置文件设计说明》§4-(3)/§5.3 守卫描述补内容判据与 15:21 实证形态、
  风险表行更新、变更记录 v1.17 行同步;
- 《配置文件单元测试说明》TC-CONFIG-061/062 行 + 当前结果 259/259 +
  扩展指南补 vfs_mark_stale_zeros()。
2026-11-05 17:08:25 +08:00

1539 lines
57 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 项目组 「数据可信」有符号零点类型:新增值类型 CFG_VAL_INT(有符号十进制,
* len 1/2/4 小端补码)——s_decode_int() 用 strtol + ERANGE +
* 宽度上下界校验,s_encode() 带负号输出;负零点偏移从此可
* 干净落盘(动因见 docs/任务规划/数据可信.md §4.4 与 §7 决策 3)
* v1.6 2026-11-05 DTU 项目组 热重载/保存视图一致性守卫(issue #73 SNTP 补验揪出)——
* poll stat→read→复 stat 三点校验 + 内容合法性 s_view_bad
* (空读或任意偏移含 NUL = 坏视图;台架 15:21 实证同尺寸
* 全 0 旧映射块可骗过纯三点校验);命中拒绝应用:RAM 与
* 基线不动、2s 重试自愈、首次自检留痕 + 每 30 趟
* WARN 限速;save 读视图与 stat 基准不符或内容坏
* 拒写(断"短/坏视图→整档
* 外科重写→非默认段被删"吞段链,台架实测 1066B→986B)。
* 台架根因为卷层 dhara 旧视图回滚(复现多形态:空读、
* 短读、同尺寸全 0 块),引擎侧无法分辨"一致但陈旧"的
* 整卷回滚——该形态重启必收敛,见设计说明
* 函数说明:引擎状态非重入——调用方须收敛同一任务(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 键解码需要,「数据可信」有符号零点类型) */
#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 uint16_t s_reject_streak; /* 连续被拒重载计数(视图不一致;0 = 无进行中
拒绝;饱和不回绕,仅用于告警限速节奏) */
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;
}
/*
* 函数功能:解析有符号十进制值(小端补码写入,「数据可信」有符号零点类型)
* 入口参数: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;
}
/*
* 函数功能:读视图内容合法性判定(stat/mtime 三点校验抓不住的内容层破视图)
* 入口参数:p_buf - s_load_file 成功缓冲 const uint8_t *
* len - 实际长度 size_t
* 返回值:1 - 视图非法(空读,或 [0,len) 内出现 NUL 字节);0 - 基本通过
* (配置文件按 INI UTF-8 文本约定生成与编辑,任何偏移都不得含 0x00:
* 引擎保存只写可打印文本,手写/FTP 文档亦然;即便出现合法前缀 +
* 尾部零填充的"部分旧块",解析器按行切分遇 NUL 即断行,零块后内容
* 本就不可达 = 静默截断同源故障,一并拒绝不误伤任何可解析文档)
* 限定条件:仅可用于"重载/保存"的既有文档上下文(boot load 首读不经此路:
* 空文件 = 合法待建形态)。真机台架 2026-11-05 实证:同尺寸整块全 0
* 旧视图可通过长度+再 stat 双检(dhara 旧映射块恰与目标同尺寸),
* 解析零错误、RAM 打回默认——首字节即 NUL,本判定 100% 拦截
*/
static int s_view_bad(const uint8_t *p_buf, size_t len) {
size_t i;
if (len == 0u) {
return 1; /* 空读:解析空文档 = 全体打回默认 */
}
for (i = 0u; i < len; i++) {
if (p_buf[i] == 0u) {
return 1; /* 全 0 旧映射块 / 含 NUL 坏块 */
}
}
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 主机侧纯逻辑单测修复) */
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;
}
/* 2026-11-05 视图一致性守卫(真机台架实证,issue #73 SNTP 补验揪出):
* stat 与 read 之间卷层可给出旧视图(dhara 缓存回滚/FTL 视图滞后)。旧实现
* 直接按"读到的视图"外科重写整档,视图偏短时非默认段随重写一并被静默删掉
* (实测 1066B → 986B,[sntp] 段整段蒸发)。读长与 stat 报长不符、或读期间
* 文件被改写 → 拒存:原文件不动、返回 -1;属主任务脏位保留 = 下一趟自然
* 重试(defaultTask 50ms 循环 / iec104 下轮 poll 投递,均自带重试语义) */
{
cfg_file_stat_t st2;
if (in_len != (size_t)st.size ||
cfg_store_stat(cfg_store_config_path(), &st2) != 0 ||
st2.size != st.size || st2.mtime != st.mtime ||
s_view_bad(p_in, in_len) != 0) {
cfg_store_free(p_in);
DBG_ERROR("save: view %luB != stat %luB, aborted (file untouched)",
(unsigned long)in_len, (unsigned long)st.size);
syslog_post(SYSLOG_SELFTEST, "cfg save view stale");
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 重试 */
}
/* 2026-11-05 视图一致性守卫(与 save 侧同族,真机台架实证 issue #73):
* 三点校验 stat → read → 再 stat + 内容合法性 s_view_bad。台架后续
* 实证(15:21):同尺寸"整块全 0 旧映射块"可骗过纯长度/再 stat 双检
* (dhara 旧映射块恰与目标同尺寸 1064B),解析零错误、RAM 照打回
* ——故必须验内容:INI UTF-8 文本任何偏移不得含 NUL(见 s_view_bad)。
* 命中形态:空读/截断/全 0 块/含 NUL 坏块/读期间改写;不一致 →
* 拒绝应用:RAM 副本原样保留、基线不刷新,下一趟(2s)重试,视图
* 恢复一致即正常应用(自愈);若持续到重启,boot 路径全新挂载必然
* 收敛。首次拒绝自检留痕 + ERROR 一行,此后每 30 趟(约 60s)一条
* WARN 计数行(限速,防日志风暴) */
{
cfg_file_stat_t st2;
int stale;
if (len != (size_t)st.size) {
stale = 1; /* 空读/截断/旧视图 */
}
else if (cfg_store_stat(cfg_store_config_path(), &st2) != 0 ||
st2.size != st.size || st2.mtime != st.mtime) {
stale = 1; /* 读期间文件消失或被改写 */
}
else if (s_view_bad(p_buf, len) != 0) {
stale = 1; /* 内容层坏视图:全 0 旧映射块/含 NUL */
}
else {
stale = 0;
}
if (stale != 0) {
cfg_store_free(p_buf);
if (s_reject_streak < 0xFFFFu) {
s_reject_streak++;
}
if (s_reject_streak == 1u) {
DBG_ERROR("poll: view %luB != stat %luB, HELD (RAM kept)",
(unsigned long)len, (unsigned long)st.size);
syslog_post(SYSLOG_SELFTEST, "cfg poll view stale");
}
else if ((s_reject_streak % 30u) == 0u) {
DBG_INFO("WARN: cfg reload held %u rounds, reboot to converge",
(unsigned)s_reject_streak);
}
return; /* 基线不刷新 = 同一变更下趟仍触发重试;RAM 不动 */
}
}
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");
}
s_reject_streak = 0u; /* 应用成功(含超长重建)= 拒绝期结束 */
/* 重载后刷新基线(回调触发的写盘不引发二次重载) */
{
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;
}