ESP32上使用miniz的tinfl_decompress处理流式解压的问题
关于miniz库tinfl_decompress流式解压的问题及解决方案
我在ESP32上使用miniz库的tinfl_decompress函数处理网络流式压缩数据,函数签名如下:
tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags);
为适配流式场景做了如下设置:
- 输出缓冲区
pOut_buf_start初始化大小为TINFL_LZ_DICT_SIZE - 每次仅向输入缓冲区传入最多512字节数据,避免一次性生成过多解压数据
但在pOut_buf_next接近输出缓冲区末尾时遇到问题:
- 继续解压会触发
TINFL_STATUS_HAS_MORE_OUTPUT状态,该状态是否真的无法恢复? - 将输出缓冲区回绕(设置
pOut_buf_next = pOut_buf_start,pOut_buf_size=TINFL_LZ_DICT_SIZE)后,下一次调用tinfl_decompress会导致输出损坏。
核心原因
tinfl_decompress依赖输出缓冲区的历史数据完成LZ77格式解压(压缩算法会引用之前已解压的数据块)。直接回绕覆盖旧数据会破坏解压器需要的历史字典,导致输出损坏。而TINFL_STATUS_HAS_MORE_OUTPUT并非无法恢复,它只是提示当前输出缓冲区剩余空间不足,需处理已输出数据后继续。
正确处理流程
1. 处理TINFL_STATUS_HAS_MORE_OUTPUT状态
- 触发该状态时立即停止当前解压调用
- 取出
pOut_buf_next到pOut_buf_start + TINFL_LZ_DICT_SIZE之间的已解压数据(如发送到应用层、写入存储) - 保留解压器需要的历史数据,而非直接回绕:
- 计算剩余需保留的历史数据长度:
TINFL_LZ_DICT_SIZE - (pOut_buf_next - pOut_buf_start) - 将缓冲区末尾的历史数据移动到起始位置
- 更新
pOut_buf_next为起始位置加上剩余历史数据的长度 - 更新
pOut_buf_size为TINFL_LZ_DICT_SIZE - (pOut_buf_next - pOut_buf_start)(即剩余可用空间)
- 计算剩余需保留的历史数据长度:
2. 允许回绕输出缓冲区的时机
只有当当前解压上下文不再需要之前的历史数据时,才能完全回绕缓冲区:
- 整个压缩数据流解压完成(返回
TINFL_STATUS_DONE) - 切换到全新的压缩数据流(需重新初始化
tinfl_decompressor结构体)
3. 流式解压循环示例
// 初始化解压上下文 tinfl_decompressor decomp_ctx; memset(&decomp_ctx, 0, sizeof(decomp_ctx)); // 输出缓冲区(大小为TINFL_LZ_DICT_SIZE) uint8_t out_buf[TINFL_LZ_DICT_SIZE]; uint8_t *out_next = out_buf; size_t out_size = sizeof(out_buf); // 输入缓冲区(每次最多512字节) uint8_t in_buf[512]; size_t in_size; const uint8_t *in_next; while (/* 还有网络数据可读 */) { // 读取网络数据到in_buf,获取实际读取长度in_size in_size = read_network_data(in_buf, sizeof(in_buf)); in_next = in_buf; while (in_size > 0) { mz_uint32 status = tinfl_decompress(&decomp_ctx, in_next, &in_size, out_buf, out_next, &out_size, TINFL_FLAG_PARSE_ZLIB_HEADER); if (status == TINFL_STATUS_HAS_MORE_OUTPUT) { // 取出已解压数据:out_next - out_buf 字节 process_decompressed_data(out_buf, out_next - out_buf); // 保留历史数据,移动缓冲区内容 size_t history_len = out_next - out_buf; if (history_len > 0) { memmove(out_buf, out_next, sizeof(out_buf) - history_len); } out_next = out_buf + (sizeof(out_buf) - history_len); out_size = history_len; // 剩余可用空间为原已使用长度 } else if (status == TINFL_STATUS_DONE) { // 处理剩余已解压数据 process_decompressed_data(out_buf, out_next - out_buf); // 当前数据流完成,可重置输出缓冲区 out_next = out_buf; out_size = sizeof(out_buf); break; } else if (status != TINFL_STATUS_OKAY) { // 处理解压错误 handle_decompress_error(status); goto exit_loop; } else { // TINFL_STATUS_OKAY:输入数据处理完毕,读取下一批网络数据 out_next += (sizeof(out_buf) - out_size); out_size = sizeof(out_buf) - (out_next - out_buf); break; } } } exit_loop: // 清理资源
关键注意事项
- 永远不要丢弃解压器依赖的历史数据,miniz的LZ解压最多需要
TINFL_LZ_DICT_SIZE字节的历史数据还原内容 - 处理
TINFL_STATUS_HAS_MORE_OUTPUT时,仅移动数据而非覆盖,确保解压上下文能访问到所需历史字典 - 处理多个独立压缩数据流时,每次新流开始前必须重新初始化
tinfl_decompressor结构体,此时可安全回绕输出缓冲区
内容的提问来源于stack exchange,提问作者Nitek
相关产品推荐
相关产品推荐

