json.c
json_check(common/json.c:28)4.2 主取值函数:json_get_value(common/json.c:47)4.3 带长度保护的变体4.4 降级时间串拆分:json_get_time_value_from_fw_temp(common/json.c:244)4.5 与任务/调度的配合5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
common/json.c所属模块:common(通用工具库) 代码行数:332
1. 文件职责
本文件是 EMS 固件里最轻量的 JSON「取值」工具集,不实现完整的 JSON 解析器(无 tokenizer、无树结构),而是把 JSON 报文当成普通字符串,用 strstr() 定位键名、再手工解析 "key":"value" 或 "key":value 的文本片段。它处在数据流的入站解析环节:云端/上位机下发的 JSON 指令、Modbus 透传的升级校验 JSON、WiFi 模组上报的 JSON,都先由 net/server.c、midware/interface/modbus/modbus.c、midware/ethernt/wireless/wireless_driver.c 收到,再调用这里的函数把关键字段抠出来。其中 json_get_value() 是全工程最热的调用之一(外部调用点约 260 处,几乎全部集中在 net/server.c 的 handle_special_cmd() 系列函数里)。json_check() 负责报文首尾大括号的粗校验,json_get_time_value_from_fw_temp() 则专门把 "YYYY/MM/DD HH:MM:SS" 形式的降级(resume)时间串拆成 6 个分量。本文件不主动驱动任何任务,纯被动调用,无状态、无全局变量、无锁。
2. 关键数据结构与宏
| 名称 | 类型(结构体/枚举/宏/联合体) | 说明 |
|---|---|---|
LEN_REQ_FILE | 宏 | 定义于 common/json.h:13,值 1024,表示单次请求文件的长度基准 |
MAX_LEN_JSON_CONNENT | 宏 | 定义于 common/json.h:14,值 LEN_REQ_FILE * 2(2048),是三个取值函数内部对「键值长度」的第一道上限 |
l_u8_json_key[256] | 局部字符数组 | json_get_value() 内 common/json.c:55,用于把 json_key 拼成带双引号的查找串 |
time_buf[10] | 局部字符数组 | json_get_time_value_from_fw_temp() 内 common/json.c:247,暂存分解出的时间字段文本 |
本文件没有定义结构体、枚举或联合体;函数统一返回 char(0/1 成功失败)或 s8(时间分量)。
3. 函数清单
| 函数名 | 作用域(static/全局) | 说明(一句话) |
|---|---|---|
json_check | 全局(json.h:17 声明) | 仅判断字符串首字符是 {、末字符是 },据此认定「是 JSON」并返回 1 |
json_get_value | 全局(json.h:18 声明) | 在 JSON 串中查找 "key":"value" 形式的字符串值并拷贝到 json_value,键值上限 MAX_LEN_JSON_CONNENT |
json_check_value | 全局(未在 json.h 声明) | 对两个字符串做 strcmp,相等返回 1;全工程无外部调用点 |
json_get_value_check_len_input | 全局(json.h:19 声明) | 同 json_get_value,但额外接受调用方传入的 len_in_MAX 做目的缓冲长度保护;全工程无外部调用点 |
json_get_int_len_input | 全局(json.h:20 声明) | 提取 "key":value 数值型字段(以 , 或 } 作为结束符),并做 len_in_MAX 保护;全工程无外部调用点 |
json_get_time_value_from_fw_temp | 全局(json.h:21 声明) | 按 Y/M/D/H/F/S 六个键从 "YYYY/MM/DD HH:MM:SS" 串中拆出年/月/日/时/分/秒 |
4. 核心逻辑解析
4.1 报文合法性粗校验:json_check(common/json.c:28)
json_check(str, len) : return (str[0]=='{' && str[len-1]=='}')
只有一条判据,不做括号配对、不做转义扫描。调用方在 net/server.c:2615 / 3442 / 3684 处均以 json_check((char *)(buf + 7), len - 7 - 2) 的形式使用:buf + 7 跳过 7 字节私有帧头,len - 7 - 2 刨掉帧头与帧尾 CRC。也就是说该函数被当作「私有协议负载是否是一个 JSON 对象」的闸门使用。
4.2 主取值函数:json_get_value(common/json.c:47)
算法分四步:
- 把
json_key两侧补上双引号拼进 256 字节的l_u8_json_key(json.c:59-61),得到查找串"key"。 json_key_start = strstr(json, l_u8_json_key)(json.c:65);json_key_end = json_key_start + strlen(json_key) + 1(json.c:66),即指向键名收尾的那个"。- 严格模式匹配(
json.c:68):要求*(json_key_end+1) == ':'且*(json_key_end+2) == '"'。注意这里把值也强制要求为双引号包裹的字符串,纯数字不加引号的{"a":1}无法取出。 - 取
json_value_start = json_key_end + 3,json_value_end = strstr(json_value_start, "\""),长度 = 两者之差;若<= MAX_LEN_JSON_CONNENT(2048)则strncpy并补'\0'返回 1,否则返回 0。
失败分支(json.c:85-89)只把 json_value[0] 置 '\0' 后返回 0;但键名未找到的那条分支(json.c:85 之前的 else)不会清空 json_value——这是与另外两个变体不一致的地方,详见第 7 节。
4.3 带长度保护的变体
json_get_value_check_len_input(common/json.c:118)与 json_get_int_len_input(common/json.c:176)共享同一套骨架,差异在于:
- 入口先做空指针与
len_in_MAX == 0检查(json.c:127、json.c:185),这是json_get_value所没有的。 - 键名不再手工拼引号,而是直接
strstr(json, json_key),然后在json.c:136/json.c:194用*(json_key_start-1)=='"'与*(json_key_end)=='"'反向确认边界。这种「先搜再验」的写法可以避免json_get_value里那种键名前后必须精确对齐的脆弱性,但也引入了向前越界读一字节的风险。 - 长度采用双重上限:先卡
MAX_LEN_JSON_CONNENT,再卡调用方给的len_in_MAX - 1;两个都是「超限即清空输出并返回 0」。 json_get_int_len_input的取值方式完全不同(json.c:196-207):json_value_start = json_key_end + 2(跳过:,指向冒号后第一个字符),结束位置取strstr(...,",")或strstr(...,"}")中先出现者。因此它是为 不加引号的数值字段设计的,例如{"ver":123,"x":1}会得到"123"。
4.4 降级时间串拆分:json_get_time_value_from_fw_temp(common/json.c:244)
输入是云端下发的断点续传(resume)时间字符串,形如 2025/08/13 12:30:45,第二个参数 time_key 是一个字符选择子:
| key | 定位方式 | 代码位置 | 返回 |
|---|---|---|---|
'Y' | fw_temp_addr 起、strstr(...,"/") 止 | json.c:254-263 | atoi(buf) % 100,即两位年 |
'M' | 第一个 / 之后、下一个 / 之前 | json.c:265-275 | 月份 |
'D' | 第二个 / 之后、" " 之前 | json.c:277-289 | 日 |
'H' | 空格之后、: 之前 | json.c:291-301 | 时 |
'F' | 第一个 : 之后、下一个 : 之前 | json.c:303-313 | 分(键名用 F 而非常见的 M,因为 M 已被月份占用) |
'S' | 串尾向前数 2 个字符 | json.c:315-324 | 秒 |
每个分支都先把子串 strncpy 进 time_buf[10] 再 atoi。调用方是 net/server.c:2715-2720,一次取齐 6 个分量填入 RESUME_BUFFER_T l_t_resume(年/月/日/时/分/秒)。
4.5 与任务/调度的配合
本文件没有任何定时器、信号量、消息邮箱或全局状态。它事实上运行在 task_net(网络任务)和 Modbus 处理任务的上下文里,函数本身不可重入性问题只来自 strstr/strncpy/atoi 这些标准库调用,编译器的 C 库实现均为纯函数,因此天然可重入。真正的时序约束完全在调用方:handle_special_cmd() 必须先 memset(g_au8FWBuf, 0, sizeof(g_au8FWBuf)) 再调用 json_get_value(),因为输出缓冲是全局的 g_au8FWBuf(net/server.c:2621 前后)。
5. 对外接口
导出(供外部调用)
| 符号 | 声明位置 | 主要调用方 |
|---|---|---|
json_check | json.h:17 | net/server.c:2615, 3442, 3684 |
json_get_value | json.h:18 | net/server.c(约 250 处,集中在 handle_special_cmd() 及各 set_xxx())、midware/interface/modbus/modbus.c:8632, 8645, 8658、midware/ethernt/wireless/wireless_driver.c:99 |
json_get_value_check_len_input | json.h:19 | 无外部调用(保留接口) |
json_get_int_len_input | json.h:20 | 无外部调用(保留接口) |
json_get_time_value_from_fw_temp | json.h:21 | net/server.c:2715-2720 |
json_check_value | 无声明(仅 json.c:99 定义) | 无 |
本文件不导出任何全局变量。
被本文件调用(外部依赖)
| 符号 | 来源 |
|---|---|
strcat / strstr / strncpy / strlen / strcmp / memset(隐式) | <string.h>(json.c:13) |
atoi | 经 includes.h 间接引入的标准库 |
u8 / s8 | includes.h → 平台类型定义 |
6. 依赖关系
- 依赖:
<string.h>、includes.h(提供u8/s8等类型)、json.h(宏与原型)。 - 被依赖:
net/server.c—— 云端通信报文解析,用json_check/json_get_value/json_get_time_value_from_fw_temp;midware/interface/modbus/modbus.c—— 升级校验 JSON 的三字段比对;midware/ethernt/wireless/wireless_driver.c—— WiFi 模组信息 JSON 取值;- 任何
#include "json.h"的模块。
7. 备注与风险点
json_check存在下溢越界读(json.c:30):参数len是unsigned int,调用方net/server.c:2615/3442/3684传的是len - 7 - 2。当收到的报文len < 9时该表达式无符号下溢成一个极大值,str[len-1]会读到缓冲区远端的随机内存。虽然这些调用点前面通常有buf == NULL || !len检查(如server.c:2612-2613),但并未卡住len < 9这一档。json_get_value失败时不清输出缓冲(json.c:85-89):其他四个取值函数在失败分支都会执行json_value[0]='\0',唯独json_get_value的「键名没找到」分支没有。调用方若不复位全局g_au8FWBuf,会读到上一轮的残留值,进而按错误的分支逻辑执行(例如误判升级对象)。json_get_value里NULL + 长度的指针运算(json.c:66):strstr返回NULL时仍执行json_key_end = json_key_start + json_key_length + 1,属未定义行为。虽然第 68 行的&&短路保证不会解引用,但依赖这一巧合。l_u8_json_key无长度检查(json.c:55-61):strcat拼接json_key,键名超过约 250 字节即栈溢出;且json_key_length声明为u8(json.c:51),键名超过 255 字节时长度被截断,后续指针运算随之出错。json_get_value_check_len_input/json_get_int_len_input有向前越界读(json.c:136、json.c:194):*(json_key_start - 1)在json_key_start恰好等于json首地址时会读到缓冲区前一个字节之外的内存。time_buf[10]在边界长度下无终止符(json.c:247):判据是time_key_length <= 10,但strncpy(time_buf, ..., 10)恰好写满 10 字节时不会补'\0',随后atoi(time_buf)会越过数组末端读取。应为<= sizeof(time_buf)-1。time_value未初始化(json.c:246):若time_key不是 6 个合法值(走default: break;),或某个strstr返回NULL导致长度判据失败跳过赋值,函数会返回一个不确定值。调用方net/server.c:2715-2720直接把它写进l_t_resume.u8_*,可能把随机值当成断点续传时间。json_get_time_value_from_fw_temp无空指针保护:strstr(fw_temp_addr, "/") + 1在格式不符时同样是对NULL做算术;'S'分支的fw_temp_addr + strlen(fw_temp_addr) - 2在字符串短于 2 字节时下溢。json_check_value是死代码:仅在json.c:99定义,json.h未声明、全工程无调用,且未加static,会占用一个全局符号槽。json_get_int_len_input的字段长度含引号:json_value_start指向冒号后第一个字符(json.c:196),对"key":"123"这种带引号的值,提取结果会连带双引号一起拷给调用方,与函数名暗示的「int」语义不符;目前无调用方,影响面为零。- 魔法数字:
json.c:66的+1、json.c:68的+1/+2、json.c:70的+3、json.c:196的+2均为硬编码偏移,缺少命名常量,报文格式一旦变化极难维护。