Flipper Zero 固件中 .ths 压缩格式(Heatshrink Data Stream)解析:从文件头定义到解码与打包实现 Flipper Zero 固件中 .ths 压缩格式Heatshrink Data Stream解析从文件头定义到解码与打包实现【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware本文以固件仓库中的文件格式文档 TarHeatshrinkFormat.md 为核心完整讲解 Flipper Zero 使用的 Heatshrink 压缩数据流HSDS容器格式7 字节文件头的逐字段定义、设备上 tar_archive.c 的解码与校验流程、hs.py 等构建工具生成.ths文件的方式以及该格式在 OTA 资源更新中的实际应用。读完本文后你可以理解.ths文件的二进制布局、复现其打包/解包过程并能在固件源码中定位该格式的关键实现。为什么需要自定义容器Heatshrink 规范不含容器格式Heatshrink 是一种基于 LZSS 的压缩库仓库中 vendored 于 lib/heatshrink/它的输出只是一条裸的压缩比特流解码器在初始化时需要知道压缩方使用的**滑窗大小window size和前瞻缓冲区大小lookahead size**这两个参数才能正确还原数据。但原始 Heatshrink 规范并没有定义一个用于存放这些压缩参数的容器格式也就是说压缩流本身是不自描述的。文档对此给出的解决方案是Flipper 自行定义了一个轻量级容器——用 7 字节的文件头magic 版本 两个压缩参数包裹 Heatshrink 压缩数据流并将该格式用于.tar归档以获得更小的文件体积和更快的 OTA 更新速度。容器内的数据流部分则完全遵循 Heatshrink 规范文件头只是参数载体。文件格式定义7 字节 HSDS 文件头文件头以魔数开始后跟版本号与压缩参数总长度为 7 字节紧随其后的就是压缩数据。逐字段定义如下均为小端存储偏移大小字段取值/含义04 字节Magic0x48 0x53 0x44 0x53ASCII 字符 HSDSHeatShrink DataStream41 字节Version版本号当前固定为0x0151 字节Window size压缩器滑窗大小对应 Heatshrink CLI 的-w参数61 字节Lookahead size压缩器前瞻缓冲区大小对应 Heatshrink CLI 的-l参数以十六进制视角看一个最小合法文件前 7 字节形如48 53 44 53 01 0D 06 └─ magic HSDS ┘ └v┘ └w┘ └l┘其中0x0D(13) 与0x06(6) 正是构建工具默认的-w 13 -l 6参数见后文 hs.py。这里有一个容易混淆的细节header 中的 window/lookahead 字节存的是2^n指数而不是窗口字节数。这在 Heatshrink 库的类型定义中可以得到印证heatshrink_encoder.h 将这两个参数注释为uint8_t window_sz2; /* 2^n size of window */ uint8_t lookahead_sz2; /* 2^n size of lookahead */因此 header 中的0x0D表示窗口实际为 2^13 8192 字节0x06表示前瞻缓冲区 2^6 64 字节。解码端必须按同样的指数语义初始化解码器这也是必须把参数放进自描述文件头的原因。设备端实现lib/toolbox/tar对 HSDS 流的解析与解压文件扩展名到打开模式的映射固件对.tar归档的统一抽象在 tar_archive.c 中。它根据扩展名决定以何种模式打开归档.ths后缀.tar HeatShrink会被映射到专用压缩读取模式tar_archive.c#L17-L29TarOpenMode tar_archive_get_mode_for_path(const char* path) { ... if(strcmp(ext, .ths) 0) { return TarOpenModeReadHeatshrink; } else { return TarOpenModeRead; } }也就是说.tar走普通文件后端可读可写.ths走 Heatshrink 流后端只读二者的选择完全由扩展名驱动。文件头的 C 侧定义与校验文档中描述的 7 字节文件头在 C 端是一个紧凑打包结构体并用编译期断言锁死了其大小tar_archive.c#L77-L86/* HSDS heatshrink data stream header magic */ static const uint32_t HEATSHRINK_MAGIC 0x53445348; typedef struct { uint32_t magic; uint8_t version; uint8_t window_sz2; uint8_t lookahead_sz2; } FURI_PACKED HeatshrinkStreamHeader; _Static_assert(sizeof(HeatshrinkStreamHeader) 7, Invalid HeatshrinkStreamHeader size);注意HEATSHRINK_MAGIC写成0x53445348在 ARM 的小端字节序下该 32 位值按地址从低到高正好存放为字节序列0x48 0x53 0x44 0x53与文档定义的 HSDS 魔数一致。FURI_PACKED确保结构体无填充因此sizeof 7的静态断言能够成立也保证 C 结构体与二进制格式逐字节对齐。打开归档时tar_archive_open会先读满 7 字节头并校验魔数任何一步失败都会关闭文件并返回 falsetar_archive.c#L174-L194if(compressed) { /* Read and validate stream header */ HeatshrinkStreamHeader header; if(storage_file_read(stream, header, sizeof(HeatshrinkStreamHeader)) ! sizeof(HeatshrinkStreamHeader)) { storage_file_close(stream); return false; } ... hs_stream-heatshrink_config.window_sz2 header.window_sz2; hs_stream-heatshrink_config.lookahead_sz2 header.lookahead_sz2; hs_stream-heatshrink_config.input_buffer_sz FILE_BLOCK_SIZE; hs_stream-decoder compress_stream_decoder_alloc( CompressTypeHeatshrink, hs_stream-heatshrink_config, file_read_cb, stream); mtar_init(archive-tar, mtar_access, heatshrink_ops, hs_stream); }这段代码体现了文件头即解码器配置的设计header 里的 window/lookahead 直接填入CompressConfigHeatshrink再连同文件读回调交给流式解码器上层 microtarmtar则像操作普通文件一样读取解压后的 tar 内容。流后端的 I/O 语义压缩流通过一组mtar_ops操作回调对接 tar 层tar_archive.c#L100-L123readcompress_stream_decoder_read按块解压解压缓冲区固定为FILE_BLOCK_SIZE512 字节write置为 NULL即压缩归档只读与明文 tar 的可写形成对比seekoffset 0时把文件指针移回 header 之后并rewind解码器否则调用compress_stream_decoder_seek。从 compress.c 的实现看compress_stream_decoder_seek内部采用读后丢弃策略——逐块解码并丢弃数据直到目标位置且furi_check(position instance-stream_position)明确不支持向后 seekcompress.c#L533-L557。这是 Heatshrink 这类有状态 LZSS 解码器的固有限制解码状态依赖此前所有已消费比特无法随机定位。对 tar 遍历场景顺序读文件头 数据块而言该限制没有实际影响。压缩参数与默认配置流式解压所需的完整配置由 compress.h 中的结构体描述/** Configuration for heatshrink compression */ typedef struct { uint16_t window_sz2; /* 滑窗大小2 的指数 */ uint16_t lookahead_sz2; /* 前瞻缓冲大小2 的指数 */ uint16_t input_buffer_sz;/* 输入/工作缓冲区大小 */ } CompressConfigHeatshrink;固件中另有一套用于图标资源的默认配置compress.c#L18-L22window_sz2 8、lookahead_sz2 4、输入缓冲 256 字节对应窗口 256 字节、前瞻 16 字节的小参数组合适合小体积图形数据。而 tar 归档场景即本文的.ths格式使用的是更大的参数构建脚本默认window_sz2 13、lookahead_sz2 6见下节在压缩率与 RAM 占用之间取得平衡。生成.ths文件构建工具链格式文档描述的是设备如何读而如何写由仓库中的 Python 构建工具完成核心是 heatshrink_stream.py 中的HeatshrinkDataStreamHeaderclass HeatshrinkDataStreamHeader: MAGIC 0x53445348 VERSION 1 def __init__(self, window_size, lookahead_size): ... def pack(self): return struct.pack( IBBB, self.MAGIC, self.VERSION, self.window_size, self.lookahead_size )struct.pack(IBBB, ...)采用小端布局4 字节无符号 int 的 MAGIC 加 3 个字节正好产出文档定义的 7 字节头unpack静态方法则校验长度7、魔数与版本号三者与设备端 C 代码的校验逻辑一一对应。hs.py命令行工具hs.py 提供了独立 CLIfbt hs包含四个子命令compress压缩任意文件为 HSDS 流参数-w/--window默认 13、-l/--lookahead默认 6、-o/--output必填。它先调用heatshrink2.compress再写入 header 压缩数据hs.py#L69-L88decompress读 7 字节头并unpack按 header 中携带的 window/lookahead 参数解压——注意参数来自文件头本身无需额外指定info仅解析并打印文件头的 window/lookahead 参数tar先把目录打成 tarball 再整体压缩为.ths。tarball 打包流程tar子命令背后的实现是 tarball.py 的compress_tree_tarball以tarfile.USTAR_FORMATFLIPPER_TAR_FORMAT在内存中生成标准 USTAR 归档通过tar_sanitizer_filter归一化 tar 元数据uid/gid 置 0、mtime 置 0、用户名为 furippa保证归档内容可复现用heatshrink2.compress(src_data, window_sz2hs_window, lookahead_sz2hs_lookahead)压缩整个 tar 数据按header 压缩流写出目标文件并返回压缩前后体积供构建系统统计。函数签名中hs_window13, hs_lookahead6正是默认的-w 13 -l 6与文件头字节0x0D 0x06吻合。实际应用场景OTA 资源更新与单元测试OTA 更新该格式最主要的落地场景是 OTA。update.py 中定义了RESOURCE_FILE_NAME resources.ths注释即 .Tar.HeatShrink更新包构建时通过compress_tree_tarball将资源目录整体压缩为.ths随包分发设备端由TarArchive按前文流程解压落盘——这正是文档开头所述smaller file sizes and faster OTA updates的具体含义整包压缩消除了逐文件压缩的开销流式解码避免了把整个解压结果载入内存。单元测试固件内置的压缩单元测试 compress_test.c 用真实.ths归档文件test.ths验证整条链路断言tar_archive_get_mode_for_path对.ths返回TarOpenModeReadHeatshrink、tar_archive_open成功打开并对照预置的 MD5 校验解压出的 tar 内容与各文件完整性compress_test.c#L242-L283。这为格式解析逻辑提供了可复现的回归验证。实现要点小结综合文档定义与两侧C 解码端 / Python 打包端实现实现或校验一个 HSDS 文件时可对照以下要点字节序全部小端C 端魔数常量0x53445348与文档的字节序列48 53 44 53是同一魔数的两种书写形式参数语义window/lookahead 存的是2^n指数实际窗口为 2^n 字节版本当前仅0x01Python 侧unpack对非 1 版本会直接抛错C 侧当前只校验魔数从源码结构看版本号字段已预留但未在解码路径上强制校验只读与顺序访问.ths流不可写、不支持向后 seek重读文件需先rewind回到 header 之后扩展名约定.ths .tar HeatShrink由 tar_archive.c 识别并切换到压缩后端可复现性打包前用 sanitizer 归一化 tar 元数据同目录内容可生成字节级一致的归档。掌握以上布局与实现路径后开发者既能用fbt hs工具链独立生成/诊断.ths文件也能在设备端源码中准确追踪从文件头解析到 tar 内容落盘的完整调用链。【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考