# psm **Repository Path**: jinleqaq/psm ## Basic Information - **Project Name**: psm - **Description**: 流式解析帧数据的方式 - **Primary Language**: C - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2026-08-31 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # PSM协议格式帧解析器 - Bug修复记录 ## 版本历史 ### v1.0.0 (2026-08-31) - 初始版本 初始发布版本,支持以下模式: - 仅帧头模式: 帧头 + 固定长度载荷 - 仅帧尾模式: 变长载荷 + 帧尾 - 帧头帧尾模式(固定载荷): 帧头 + N字节载荷 + 帧尾 - 帧头帧尾模式(变长载荷): 帧头 + 变长载荷 + 帧尾 --- ### v1.1.0 (2026-08-31) - Bug修复版本 #### Bug #1 [严重] 帧尾匹配失败时丢失当前字节 **状态**: PSM_STATE_TAIL_MATCHING **问题**: 当固定长度载荷已收满并进入帧尾匹配状态后,如果某个字节不匹配帧尾, 直接调用 `vResetState()` 重置整个解析器。当前字节从环形缓冲区读出后直接丢弃, 如果该字节恰好是下一个帧的帧头起始字节,则会导致丢帧。 **修复**: 帧尾匹配失败时,先完全重置状态,然后调用新增的 `vTryResync()` 函数, 尝试将当前字节作为新帧的帧头起始进行重新同步。 **影响场景**:帧格式: Header(0x55) + Payload(2) + Tail(0xAA 0xBB) 收到: 0x55, 0x01, 0x02, 0xAA, 0x55, ... ^--- 帧尾第2字节不匹配,0x55是新帧头 v1.0.0: 0x55被丢弃,后续帧无法解析 v1.1.0: 0x55被识别为新帧头,正确进入HEADER_MATCHING状态 --- #### Bug #2 [严重] 帧头匹配失败回退逻辑不完整(KMP回退) **状态**: PSM_STATE_HEADER_MATCHING **问题**: 当帧头匹配到一半失败时,原代码仅检查当前字节是否等于 `header[0]`, 没有考虑帧头模式内部的前缀后缀重叠。这会导致在特定字节流模式下遗漏有效的帧头起始点。 **修复**: 新增 `u8HeaderFallback()` 辅助函数,实现KMP算法的失败回退机制。 当帧头匹配失败时,计算已匹配部分的最长前缀后缀重叠长度,找到正确的回退位置, 而不是简单地回到 `header[0]`。 **影响场景**:帧头: {0xAA, 0xAA, 0xBB} 字节流: 0xAA, 0xAA, 0xAA, 0xBB v1.0.0: 匹配 0xAA(header[0]) -> header_idx=1 匹配 0xAA(header[1]) -> header_idx=2 0xAA != header[2](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/13749/.vscode/extensions/marscode.marscode-extension-1.7.8/0xBB), 但 0xAA == header[0] -> header_idx=1 0xBB != header[1](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/13749/.vscode/extensions/marscode.marscode-extension-1.7.8/0xAA), 且 0xBB != header[0] -> 重置,帧头丢失! v1.1.0: 匹配 0xAA(header[0]) -> header_idx=1 匹配 0xAA(header[1]) -> header_idx=2 0xAA != header[2](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/13749/.vscode/extensions/marscode.marscode-extension-1.7.8/0xBB) -> u8HeaderFallback计算: {0xAA,0xAA}的最长前缀后缀重叠为{0xAA},长度1 -> 0xAA == header[1] -> header_idx=2 0xBB == header[2] -> header_idx=3, 帧头匹配成功! --- #### Bug #6 [中等] 内部函数命名语义不清 **问题**: `vResetStateMachine()` 和 `vResetState()` 命名过于相似, 容易混淆两者的区别(前者保留data_idx,后者清除data_idx)。 **修复**: - `vResetStateMachine()` -> `vResetForNextFrame()`: 语义明确为"准备下一帧,保留数据" - `vResetState()` -> `vResetAll()`: 语义明确为"完全重置,丢弃所有数据" --- #### Bug #7 [轻微] bPsm_Init变长模式缺少data_buf_size校验 **问题**: 在变长载荷模式(payload_len==0, tail_len>0)下, 没有校验 `data_buf_size` 是否足够容纳帧尾检测所需的最少字节数。 如果 `data_buf_size < tail_len`,则 `bDetectTail()` 永远无法返回true, 解析器将永远无法成功解析任何帧。 **修复**: 在 `bPsm_Init()` 中增加校验: ```c if (config->payload_len == 0 && config->tail_len > 0 && config->data_buf_size < config->tail_len) { return false; } ``` --- #### Bug #8 [轻微] 固定长度模式缺少运行时缓冲区溢出保护 **状态**: PSM_STATE_DATA (payload_len > 0 分支) **问题**: 固定长度模式下,代码直接写入 `data_buf[data_idx]` 而没有检查 `data_idx < data_buf_size`。虽然 `bPsm_Init()` 中有 `payload_len <= data_buf_size` 的校验,但这依赖初始化校验来保证运行时安全。如果有人绕过 `bPsm_Init()` 直接设置字段,或未来代码修改引入不一致,就会导致缓冲区溢出。 **修复**: 在固定长度模式的DATA状态中,写入前增加运行时溢出检查: ```c if (ctx->data_idx >= ctx->data_buf_size) { vResetAll(ctx); break; } ``` --- --- ### v1.2.0 (2026-08-31) - 配置校验与防御性修复 #### Bug #9 [中等] bPsm_Init 允许"无帧头+固定载荷"歧义配置 **问题**: 当 `header_len == 0 && payload_len > 0` 时, 无帧头分支会误用变长模式的 帧尾检测逻辑(bDetectTail), 而DATA状态又按固定长度收取载荷, 两者语义矛盾。 在 `tail_len == 1` 且首字节恰为帧尾字节时, 会产生错误的空帧并静默丢弃该字节。 **修复**: 在 `bPsm_Init()` 中增加校验, 无帧头时必须为变长载荷(由帧尾定界): ```c if (config->header_len == 0 && config->payload_len > 0) { return false; } ``` #### Bug #10 [轻微] u8HeaderFallback 的 uint8_t 下溢越界读 **问题**: 当 `matched_len == 0` 时, `for (uint8_t k = matched_len - 1; ...)` 中 `matched_len - 1` 下溢为 255, 循环体越界读 `header[255]`。 当前调用路径(HEADER_MATCHING中 header_idx >= 1)不会触发, 属防御性缺陷。 **修复**: 函数入口增加 `matched_len < 2` 提前返回(0或1字节不存在前缀后缀重叠)。 #### Bug #11 [轻微] vTryResync 无帧头分支缺少防御检查 **问题**: 无帧头分支直接写 `data_buf[0]` 未检查 `data_buf_size`; 且固定载荷模式下误做变长模式的空帧检测, `tail_len == 1` 时当前字节会被 误判为空帧帧尾而静默丢弃(该字节本应是新帧的第一个载荷字节)。 **修复**: - 写入前检查 `data_buf_size >= 1`; - 空帧检测(bDetectTail)仅限变长模式(`payload_len == 0`), 固定载荷模式 由DATA状态按 `payload_len` 收满后再进入TAIL_MATCHING。 --- ### 测试更新 (v1.2.0) - **测试28/29/40 断言修正**: 原断言期望"帧尾被破坏的第一帧"解析成功, 与 帧协议语义(帧尾=完整性校验)矛盾。已改为验证"坏帧丢弃 + 当前字节重同步 + 后续帧正确解析"(与测试27语义对齐)。 - **测试41**: [Bug#9] Init 拒绝"无帧头+固定载荷"配置 - **测试42**: [Bug#9] 回归-仅帧尾变长模式不受影响 - **测试43**: [Bug#10回归] KMP 多级回退(帧头 {0xAA,0xAA,0xBB,0xAA}) - **测试44**: 随机回环压力测试(100轮 x 4模式 x 504帧, 帧数与内容全部匹配) --- ### 补充改进 (v1.2.0) - **vTryResync 对称性**: 多字节帧头重同步分支显式重置 `data_idx = 0`, 与单字节帧头分支保持一致(调用前 `vResetAll` 已清零, 属防御性冗余)。 - **随机测试重试上限**: `test_random_loopback()` 生成变长载荷时增加 `retry_count < 100` 上限, 防止极端情况下无界重试(实际由 `len < seqlen` 保护不会死循环)。 - **vPsm_Reset 注释说明**: 头文件注释明确"仅复位状态机, 不清除环形缓冲区, 调用后 bPsm_Parse 会继续处理已有数据; 如需清空请调用 vRingBuf_Clear"。 --- ### 已知限制 (v1.2.0 说明, 不影响正确性) - **[性能]**: `u8HeaderFallback()` 每次匹配失败时以 O(k^2) 重新计算最长前后缀, 最坏 O(n^3) (n=header_len, 上限255)。实际帧头通常2~4字节, 影响可忽略; 如需极致性能可预计算 KMP 失败函数表。 - **[设计]**: TAIL_MATCHING 失败时, 已匹配的帧尾前缀字节无法回滚(流式单字节 消费, 无回退缓冲)。协议设计时应避免帧尾字节与帧头/载荷字节混淆。 - **[设计]**: 变长模式(仅帧尾定界)下, 载荷中若包含帧尾序列会产生假帧尾, 需要在协议层使用转义或长度字段规避。 --- ## 新增内部函数 | 函数 | 说明 | | -------------------- | ------------------------------------ | | `u8HeaderFallback()` | 计算帧头匹配失败时的KMP回退位置 | | `vTryResync()` | 帧尾匹配失败后尝试用当前字节重新同步 | ## 重命名内部函数 | 旧名称 | 新名称 | 说明 | | ---------------------- | ---------------------- | --------------------------- | | `vResetStateMachine()` | `vResetForNextFrame()` | 保留data_idx,帧成功后调用 | | `vResetState()` | `vResetAll()` | 清除data_idx,异常恢复时调用 | ## 向后兼容性 - **外部API完全兼容**: 所有公开函数签名和语义均未改变 - **内部函数重命名**: `vResetStateMachine`/`vResetState` 为static函数,不影响外部调用 - **现有测试用例**: 所有26个测试用例预期均可正常通过 - **新增校验**: `bPsm_Init()` 新增的校验只会拒绝之前就不合理的配置