尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

crosspoint-reader 固件缓存文件格式完全解析:book.bin、section.bin 与 CLX1 库索引

发布时间:2026/9/26 8:32:03

资讯中心
01
ARTICLE

crosspoint-reader 固件缓存文件格式完全解析:book.bin、section.bin 与 CLX1 库索引

crosspoint-reader 固件缓存文件格式完全解析:book.bin、section.bin 与 CLX1 库索引
【免费下载链接】crosspoint-readerOpen-source e-reader firmware项目地址https://gitcode.com/gh_mirrors/cr/crosspoint-reader点击查看免费下载crosspoint-reader 是一套开源电子书阅读器固件为了让 ESP32 上的阅读体验足够快它把 EPUB 解析、排版与书库扫描的中间产物以紧凑的二进制格式缓存在 SD 卡上。本篇技术指南围绕仓库中的 docs/file-formats.md 展开系统讲解三类核心缓存文件的字节级布局与版本演进机制book.binEPUB 元数据与 spine/TOC 查找表、section.bin逐章节排版页面缓存以及 CLX1library.idx整卡书库索引。读完本文你将能使用 ImHex 模式逐字节解析这些缓存、理解缓存版本号为何如此频繁地跳动、以及书库索引为何能在一张 SD 卡上秒级完成数千本书的排序与检索。总体设计POD 序列化与字符串约定所有缓存文件都位于 SD 卡/.crosspoint/目录下的epub_hash/子目录中每本书一个目录。文件内的所有 PODPlain Old Data字段都使用 ESP32 的小端表示法little-endian字符串则是长度前缀 UTF-8 字节的结构。这套约定由 lib/Serialization/Serialization.h 实现writePod/readPod按sizeof(T)直接读写结构体或标量的原始内存字节writeString/readString先写一个uint32_t字节长度再写入原始 UTF-8 字节读取时先读长度再resize读取。inline void writeString(HalFile file, const std::string s) { const uint32_t len s.size(); writePod(file, len); file.write(reinterpret_castconst uint8_t*(s.data()), len); } inline void readString(HalFile file, std::string s) { uint32_t len; readPod(file, len); s.resize(len); file.read(s[0], len); }从源码看这些模板同时针对std::ostream和HalFileSD 文件句柄实例化因此构建缓存时的临时文件流与正式文件读写可以共用同一套编解码函数。理解这个约定是阅读下文所有格式的前提凡是 String 字段第一个 4 字节是长度后面才是数据。book.binEPUB 元数据与 spine/TOC 查找表Version 10book.bin存储一本 EPUB 的元数据以及 spine阅读顺序与 TOC目录两张查找表LUT。当前固件由BookMetadataCachelib/Epub/Epub/BookMetadataCache.h、lib/Epub/Epub/BookMetadataCache.cpp写入版本号为10源码中BOOK_CACHE_VERSION 10注释说明 v10 的作用是忽略有歧义的 guide 文本引用。文件布局文件头部固定为u8 version —— 必须为 10否则整体拒绝加载 u32 lutOffset —— 查找表区的起始偏移 u16 spineCount —— spine 条目数 u16 tocCount —— TOC 条目数 Metadata metadata —— 5 个长度前缀字符串title / author / language / coverItemHref / textReferenceHref随后在lutOffset处是两张连续的 LUTspineLut[spineCount]与tocLut[tocCount]每个元素是一个u32文件偏移指向条目真正的存储位置。LUT 之后才是按顺序排列的SpineEntry与TocEntry本体。各结构字段与源码中BookMetadataCache::SpineEntry、TocEntry一一对应结构字段类型说明Metadatatitle / author / languageString书名、作者、语言代码MetadatacoverItemHrefString封面图片路径MetadatatextReferenceHrefString引导式阅读guided reading首个文本引用路径SpineEntryhrefStringspine 资源路径SpineEntrycumulativeSizeu32截至本条目的累计解压后 spine 字节数进度/百分比换算的关键SpineEntrytocIndexs16对应 TOC 条目索引若无直接对应条目则为 -1取前一条目的 TOC 索引继承TocEntrytitle / href / anchorString章节标题、资源路径、片段标识符fragmentTocEntrylevelu8嵌套层级TocEntryspineIndexs16指向 spine 的索引无则为 -1ImHex 模式仓库为每个格式提供了可直接用于 ImHex 十六进制编辑器的解析模式patternbook.bin的完整模式如下import std.mem; import std.string; import std.core; #define EXPECTED_VERSION 10 #define MAX_STRING_LENGTH 65535 struct String { u32 length [[hidden, comment(String byte length)]]; if (length MAX_STRING_LENGTH) { std::warning(std::format(Unusually large string length: {} bytes, length)); } char data[length] [[comment(UTF-8 string data)]]; } [[sealed, format(format_string), comment(Length-prefixed UTF-8 string)]]; fn format_string(String s) { return s.data; }; struct Metadata { String title [[comment(Book title)]]; String author [[comment(Book author)]]; String language [[comment(Book language code)]]; String coverItemHref [[comment(Path to cover image)]]; String textReferenceHref [[comment(Path to guided first text reference)]]; }; struct SpineEntry { String href [[comment(Resource path)]]; u32 cumulativeSize [[comment(Cumulative uncompressed spine size through this entry)]]; s16 tocIndex [[comment(Index into TOC, or inherited/previous TOC index when no direct entry exists)]]; }; struct TocEntry { String title [[comment(Chapter/section title)]]; String href [[comment(Resource path)]]; String anchor [[comment(Fragment identifier)]]; u8 level [[comment(Nesting level)]]; s16 spineIndex [[comment(Index into spine (-1 if none))]]; }; struct BookBin { u8 version; if (version ! EXPECTED_VERSION) { std::error(std::format(Unsupported version: {} (expected {}), version, EXPECTED_VERSION)); } u32 lutOffset [[comment(Offset to lookup tables)]]; u16 spineCount; u16 tocCount; Metadata metadata; u32 currentOffset $; if (currentOffset ! lutOffset) { std::warning(std::format(LUT offset mismatch: expected 0x{:X}, got 0x{:X}, lutOffset, currentOffset)); } u32 spineLut[spineCount] [[comment(Spine entry offsets)]]; u32 tocLut[tocCount] [[comment(TOC entry offsets)]]; SpineEntry spines[spineCount]; TocEntry toc[tocCount]; }; BookBin book 0x00; u32 fileSize std::mem::size(); u32 parsedSize $; if (parsedSize ! fileSize) { std::warning(std::format(Unparsed data detected: {} bytes remaining at offset 0x{:X}, fileSize - parsedSize, parsedSize)); }源码级实现细节阅读源码可以补充几个模式看不出的关键实现事实两趟式构建two-pass build构建时先把 spine 条目流式写入spine.bin.tmpTOC 条目写入toc.bin.tmp最后由buildBookBin()把二者连同 LUT 合并成正式的book.bin见 BookMetadataCache.cpp。整个构建过程使用 3 个 4KB 缓冲的BufferedFileWriter/Reader原因是 spine 与 TOC 两趟的写入会与 zip 解压的 SD 读交错进行无缓冲时每个 4 字节 POD 都触发一次 512 字节扇区事务实测一本 1732 个 spine 的合集书需要 31 秒缓冲后降到 SD 速度级别。累计大小cumulativeSize构建时通过ZipFile查询每个 spine 资源解压后的大小并累加。对于大型 EPUBspineCount 400即LARGE_SPINE_THRESHOLD固件不再把整个 ZIP 中央目录载入内存ESP32-C3 只有约 380KB RAM2000 章节会 OOM而是对 spine 路径做 FNV-1a 哈希后一次性批量匹配 ZIP 条目实现 O(n·log m) 的查找。读取端的 RAM 缓存load()时会把全部 spine 的cumulativeSize顺序读入一个std::vectoruint32_t。这样进度条每次渲染与百分比跳转都是 O(1) 查询避免每次访问都做 2 次 seek 加一次堆分配的 SpineEntry 读取。TOC 标题预组合createTocEntry()在写入前对标题调用utf8ComposeNfc()进行 NFC 规范化因为设备字体没有组合字符定位能力NFD 标题会渲染错乱。短写保护buildBookBin()结束时若 flush 失败卡满/拔卡会主动删除被截断的book.bin否则截断文件仍能通过版本检查导致下次打开错误地复用坏缓存。section.bin逐章节排版页面缓存Version 47每个已排版的 spine 章节对应sections/*.bin目录下的一个文件存储该章节的全部渲染页面。当前版本为47源码 lib/Epub/Epub/Section.cpp 中SECTION_FILE_VERSION 47注释明确写着v47: 头部加入字距与词距字段用于缓存校验缓存的 BlockStyle 只存字距。头部即缓存失效键section.bin的头部字段就是整套排版参数的快照任何影响布局的设置与当前阅读器设置不一致该章节缓存就会被丢弃并重建。这一点在 lib/Epub/Epub/ReaderRenderSpec.h 中有直接对应——ReaderRenderSpec的每个字段都参与缓存校验struct ReaderRenderSpec { int fontId 0; float lineCompression 1.0f; bool extraParagraphSpacing false; int8_t characterSpacing 0; uint8_t wordSpacingPercent 100; uint8_t paragraphAlignment 0; uint16_t viewportWidth 0; uint16_t viewportHeight 0; bool hyphenationEnabled false; bool embeddedStyle true; uint8_t imageRendering 0; bool focusReadingEnabled false; };该结构通过CrossPointSettings::readerRenderSpec(width, height)构造视口参数由调用方传入因此一个 spec 永远不会处于半填充状态。对应到section.bin头部v47s32 fontId float lineCompression bool extraParagraphSpacing u8 paragraphAlignment u16 viewportWidth u16 viewportHeight bool hyphenationEnabled bool embeddedStyle u8 imageRendering bool focusReadingEnabled s8 characterSpacing ← v47 新增 u8 wordSpacingPercent ← v47 新增v47 新增的两个字段有符号的characterSpacing字距单位像素与无符号的wordSpacingPercent词距百分比都参与缓存校验。同时每个TextBlock的BlockStyle在directionDefined之后只序列化characterSpacing词距则在排版时被解析进缓存的词位置word position中——这就是早期版本章节文件需要重建的原因。版本演进史为什么版本号一直跳动section.bin的版本号历史就是一份排版引擎演进日志。逐条列出从新到旧v47当前头部加入字距/词距字段如上所述。v46保持 v45 序列化布局不变仅因有序列表开始编号、list-style-type: none抑制列表标记、ul/ol容器把自身 margin 与 padding 贡献给子块 inset而升版——这些改变缓存的词内容与页面布局。v45保持 v44 布局不变因内部 EPUB 链接现在保留 CSS 上标/下标样式改变缓存词样式标志与页面布局。v44把排版过程中产生的**内部链接矩形internal-link rectangles**追加进每个序列化页面阅读器用这些矩形做触摸导航旧缓存没有链接几何数据必须重建。v43保持 v42 布局不变因段落基础方向base direction现在排除行内元素引入的方向变化。v42保持 v41 布局不变因闭合块现在剥离继承的垂直 margin 与 padding。v41保持 v40 布局不变因简单 HTML 表格行改为按定位列positioned columns排版而非带合成行/单元格标签的扁平段落。v40保持 v39 布局不变因大文本块被软刷新soft-flush时 ruby 组现在保持完整。v39保持 v38 布局不变因图片顶部 margin 现在被钳制clamp保证全高图片不超出页面视口。v38保持 v37 布局不变因专注阅读Focus Reading现在允许在可见连字符和破折号处断行并把焦点切分词整体断词改变缓存页面布局。v37固定大小脚注 href 字段从 96 字节增至 256 字节每条脚注记录从 128 字节变为 288 字节旧缓存必须作废重建。v36保持 v35 布局不变因ruby、两端对齐文本定位与 CJK 断行改用修正后的词测量v35 缓存的页面布局不再匹配。v35头部新增 offset并为每页增加一个uint32_t的可见文本偏移 LUT条目其余 LUT 不变。v34与 v33 二进制相同。升版因词距抑制收窄为仅在源中粘连的 token 之间生效v33 会丢弃任何在 CJK 断行点相遇的两个词之间的空隙导致韩文Hangul词间空格被折叠v33 词位置不再匹配排版引擎现在的结果。v30与 v29 二进制相同。升版因阿拉伯语上下文连字成形contextual shaping改变了文本测量——getTextAdvanceX现在测量成形后的视觉文本v29 缓存的词位置不再与drawText渲染一致。v28引入序列化的词样式位下划线、删除线、上标、下标。该版本同时确立了格式的大部分骨架段落对齐、断词、内嵌 CSS、图片渲染模式、专注阅读的缓存失效字段页面偏移 LUT每页可见文本偏移 LUTbody内从零开始的 Unicode 码点用于片段/脚注导航的 anchor→page 映射为导航与旧版同步回退保留的段落与列表项 LUT可选的逐词专注阅读切分元数据每页脚注条目下划线/删除线/上标/下标的序列化词样式位扁平 TextBlock 词存储v29每词数组加一个共享的 NUL 结尾文本 blob取代 v28 的长度前缀词字符串。磁盘顺序与内存 arena 一致固件只需一次分配、一次 SD 读就能读入整个块载荷。可见版本的每一次跳动都与缓存词位置/页面布局不再与排版引擎当前输出匹配严格对应——这正是缓存校验机制的核心宁可重建不可错用。ImHex 模式section.binv47的完整 ImHex 模式如下import std.mem; import std.string; import std.core; #define EXPECTED_VERSION 47 #define MAX_STRING_LENGTH 65535 #define FOOTNOTE_NUMBER_LEN 32 #define FOOTNOTE_HREF_LEN 256 struct String { u32 length [[hidden, comment(String byte length)]]; if (length MAX_STRING_LENGTH) { std::warning(std::format(Unusually large string length: {} bytes, length)); } char data[length] [[comment(UTF-8 string data)]]; } [[sealed, format(format_string), comment(Length-prefixed UTF-8 string)]]; fn format_string(String s) { return s.data; }; enum PageElementTag : u8 { TAG_PageLine 1, TAG_PageImage 2, TAG_PageHorizontalRule 3 }; enum WordStyle : u8 { REGULAR 0, BOLD 1, ITALIC 2, BOLD_ITALIC 3, UNDERLINE 4, STRIKETHROUGH 8, SUP 16, SUB 32 }; enum TextAlign : u8 { JUSTIFIED 0, LEFT_ALIGN 1, CENTER_ALIGN 2, RIGHT_ALIGN 3, NONE 4 }; struct BlockStyle { TextAlign alignment; bool textAlignDefined; s16 marginTop; s16 marginBottom; s16 marginLeft; s16 marginRight; s16 paddingTop; s16 paddingBottom; s16 paddingLeft; s16 paddingRight; s16 textIndent; bool textIndentDefined; bool isRtl; bool directionDefined; s8 characterSpacing; }; struct TextBlock { u16 wordCount; u8 hasFocus; u16 textBytes [[comment(Total size of text[], including one NUL per word)]]; if (wordCount 0) { u16 textOff[wordCount] [[comment(Byte offset of word is text within text[])]]; s16 wordXPos[wordCount]; if (hasFocus ! 0) { u16 wordFocusSuffixX[wordCount] [[comment(Suffix x offset from word start)]]; } WordStyle wordStyle[wordCount]; if (hasFocus ! 0) { u8 wordFocusBoundary[wordCount] [[comment(UTF-8 byte boundary between bold prefix and suffix)]]; } char text[textBytes] [[comment(All words back to back, each NUL-terminated)]]; } BlockStyle blockStyle; }; struct ImageBlock { String imagePath; String srcPath; s16 width; s16 height; }; struct PageLine { s16 xPos; s16 yPos; TextBlock block; }; struct PageImage { s16 xPos; s16 yPos; ImageBlock image; }; struct PageHorizontalRule { s16 xPos; s16 yPos; u16 width; u8 thickness; }; struct PageElement { PageElementTag pageElementType; if (pageElementType TAG_PageLine) { PageLine pageLine [[inline]]; } else if (pageElementType TAG_PageImage) { PageImage pageImage [[inline]]; } else if (pageElementType TAG_PageHorizontalRule) { PageHorizontalRule horizontalRule [[inline]]; } else { std::error(std::format(Unknown page element type: {}, pageElementType)); } }; struct FootnoteEntry { char number[FOOTNOTE_NUMBER_LEN]; char href[FOOTNOTE_HREF_LEN]; }; struct Page { u16 elementCount; PageElement elements[elementCount] [[inline]]; u16 footnoteCount; FootnoteEntry footnotes[footnoteCount]; }; struct AnchorEntry { String anchor; u16 page; }; struct AnchorMap { u16 count; AnchorEntry entries[count]; }; struct ParagraphLut { u16 count; u16 paragraphIndex[count]; }; struct SectionBin { u8 version; if (version ! EXPECTED_VERSION) { std::error(std::format(Unsupported version: {} (expected {}), version, EXPECTED_VERSION)); } s32 fontId; float lineCompression; bool extraParagraphSpacing; u8 paragraphAlignment; u16 viewportWidth; u16 viewportHeight; bool hyphenationEnabled; bool embeddedStyle; u8 imageRendering; bool focusReadingEnabled; s8 characterSpacing; u8 wordSpacingPercent; u16 pageCount; u32 pageLutOffset; u32 anchorMapOffset; u32 paragraphLutOffset; u32 listItemLutOffset; u32 visibleTextLutOffset; Page pages[pageCount]; u32 currentOffset $; if (currentOffset ! pageLutOffset) { std::warning(std::format(Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}, pageLutOffset, currentOffset)); } u32 pageLut[pageCount] [[comment(Page data offsets)]]; if (anchorMapOffset ! 0) { AnchorMap anchorMap anchorMapOffset; } if (paragraphLutOffset ! 0) { ParagraphLut paragraphLut paragraphLutOffset; } if (listItemLutOffset ! 0 paragraphLutOffset ! 0) { u16 listItemIndex[paragraphLut.count] listItemLutOffset; } if (visibleTextLutOffset ! 0) { u32 visibleTextOffset[pageCount] visibleTextLutOffset; } }; SectionBin section 0x00; u32 fileSize std::mem::size(); u32 parsedSize $; if (parsedSize ! fileSize) { std::warning(std::format(Unparsed data detected: {} bytes remaining at offset 0x{:X}, fileSize - parsedSize, parsedSize)); }页面内容编码要点三种页面元素PageElement用 1 字节 tag 区分三种元素——TAG_PageLine文本行含PageLine坐标 TextBlock、TAG_PageImage图片含PageImage坐标 ImageBlockImageBlock 里保存图片路径、源路径与尺寸、TAG_PageHorizontalRule水平线坐标 宽度 粗细。TextBlock 的扁平词存储textBytes之后的text[]是把所有词首尾相连、每个词以 NUL 结尾的单个 blob每个词的文本偏移记录在textOff[wordCount]中。这是 v29 起引入的设计让固件能用一次分配 一次 SD 读载入整个块载荷。专注阅读Focus Reading附加数据当hasFocus ! 0时每个词附带wordFocusSuffixX后缀相对词首的 x 偏移与wordFocusBoundary加粗前缀与后缀之间的 UTF-8 字节边界实现逐词聚焦切分的缓存。词样式位WordStyle使用位标志——BOLD1、ITALIC2、UNDERLINE4、STRIKETHROUGH8、SUP16、SUB32BOLD_ITALIC3为组合值可组合存储。脚注每页的脚注条目为固定大小的number[32]href[256]即每条 288 字节v37 扩容后。Anchor 与 LUTanchor 映射anchor→page、段落 LUT、列表项 LUT、可见文本偏移 LUT 均由头部偏移字段定位偏移为 0 表示该表不存在visibleTextOffset[pageCount]记录每页起点在body中从零计数的 Unicode 码点偏移。增量构建与部分文件源码佐证lib/Epub/Epub/Section.h 揭示了该格式在固件运行期的使用方式章节排版支持增量构建incremental buildstartBuild()启动、buildSomeMore(N)每次多排版若干页大章节的第一页可以立即显示UI 保持响应构建先写入filePath .part临时文件提交commit时才原子换名覆盖正式文件因此重建期间旧缓存始终可读构建被中断退出、休眠、导航时调用suspendBuild()把已完成的页面作为**部分文件partial**持久化——版本哨兵 LUT 水印尾注watermark trailer下次打开该章节可立即显示已有页面并后台续建partial_标志表示pageCount是水印而非章节总页数页面统一读取loadPage(int)优先从活动构建取若已排到该页否则从磁盘文件读finalized 或 partial提供findAnchor()/getPageForAnchor()anchor→页码、getPageForParagraphIndex()XPathp[N]导航、getPageForListItemIndex()列表项导航、getVisibleTextOffsetForPage()与getPageForVisibleTextOffset()精确可见文本偏移↔页码双向换算等导航 API全部由头部各 LUT 支撑。CLX1库索引.crosspoint/library.idxCLX1 是整张 SD 卡的书库索引由 lib/LibraryIndex/LibraryBuilder.cpp 写入、lib/LibraryIndex/LibraryIndexFile.cpp 读取一份文件描述卡上所有书书架shelf无需打开任何一本书即可对数以千计的标题排序与搜索。格式版本为2CLIX_FORMAT_VERSION。版本不符即校验失败并重建这就是全部的迁移机制——没有任何兼容旧版本的代码路径旧索引在打开时被拒绝lib/LibraryIndex/LibraryFormat.h 中的ClixValidity::UnknownFormatVersion然后由构建器重建。文件布局所有区段按 512 字节对齐每个区段都落在 SD 块边界上SectionOffsetContentsHeader064 字节ClixHeaderFoldersfolderStart长度前缀路径每个文件夹一条RecordsrecordStartbookCount× 128 字节的ClixRecordPermutationspermStart先是bookCount个 u16 作者序再是bookCount个 u16 到达序Name blobnameStart每条记录路径哈希、文件名、规范作者、标题、源作者见下文到达序arrival permutation按最旧在前排列键为记录的 FAT 修改时间文件落到卡上的时刻firstSeen构建分配的出现计数器用于打破平局并覆盖那些文件系统不报告时间的书。fold 版本 3 引入了时间戳键fold 版本升级会重建排序ranks同时保留firstSeen。区段偏移完全由计数推导layoutSections()依据bookCount、文件夹字节数与名字字节数一次性算出全部偏移和预期文件大小写方与读方因此永远不可能对某区段从哪开始产生分歧LibraryFormat.h。记录恰好 128 字节128 字节的**固定步长stride**是 CLX1 承重结构阅读器可直接 seek 到第 n 条记录recordOffset(h, n) recordStart 128n无需偏移表32 条记录恰好填满一个 4096 字节缓冲4096 % 128 0一次流式扫描永不跨记录由于recordStart512 对齐且 128 整除 512每次块读取天然对齐static_assert(sizeof(ClixRecord) 128)强制约束见 LibraryFormat.h 与 test/library_format/LibraryFormatTest.cpp。ClixRecord内部字段#pragma pack(push,1)字段类型说明nameOffu32相对nameStart的姓名 blob 偏移fileSizeu32目录项打开时捕获的文件大小属于身份identity的一部分firstSeenu16构建分配的出现计数器folderIdu16所属文件夹序号nameLen / foldLen / authorKeyLenu8 ×3各字段长度metadataStatusu8元数据提取状态未尝试 / 已提取 / 失败fold[96]char[96]规范化后的标题用于搜索与排序authorKey[12]char[12]作者词的折叠排序键modificationTimeu32SdFat 返回的打包 FAT 修改日期时间每个记录携带fold[96]——标题被规范化去除重音、转小写、移除前置冠词以及authorKey[12]——作者词被折叠排序因此Victor Hugo与Hugo Victor归并为同一人。authorKey是分组键而非排序键书架按姓排序姓是另行从显示名推导的。记录中 metadataStatus 前一字节之前的字段占用了 v1 的 alignment 与保留字节因此记录保持恰好 128 字节最后 4 字节是 SdFat 返回的打包 FAT 修改日期时间零时间戳不可信。头部还记录本次构建是否启用了 EPUB 元数据提取metadataEnabled防止关闭元数据重建时文件名回退看起来对后来启用元数据的构建很新鲜。Name blob每条记录的名字区位于nameStart nameOff布局为[u64 pathHash] FNV-1a 指纹完整路径的 64 位哈希 [nameLen bytes] 文件名不含目录 [u8][author] 显示作者每个 authorKey 在整库范围内投票选出一个拼写 [u8][title] 书自己的标题从未给出则为长度 0 [u8][source] 清洗后的作者拼写在整库拼写投票之前两个值得强调的设计约束文档中明确指出是真实缺陷换来的教训文件名必须是第一个文本字段且必须是文件名readPath从它重建书籍完整路径若把显示标题写在这里书将永远无法打开。这就是标题要有独立字段的原因。源作者与显示规范作者分离这样后续增删书后的重建可以重复进行拼写投票而已有显示读取仍然止步于作者或标题字段并保留偏移。FNV-1a 64 位路径哈希由clixPathHash()计算构建器的对账reconciliation与浏览器最近阅读查找共用同一函数二者必须对同一路径逐字节一致LibraryFormat.h。新鲜度与不变重建Freshness对账机制把持久化的 64 位完整路径指纹当作书籍身份。只有当指纹、大小、非零 FAT 时间戳、fold 版本、元数据模式与预期提取状态全部一致时元数据才被复用。零时间戳或上次提取失败的 EPUB 会被重新解析。若每个现存记录都复用了元数据、新旧计数一致、且没有看到不可读条目则暂存文件被丢弃活动索引保持逐字节不变。因此一次常规重建行为本质上是新鲜度检查而非强制重读元数据。头部标志位与降级语义ClixHeader.flags定义两个降级标志RANKS_DEGRADED一个或多个排序序退化为遍历序walk order因为一次可检查的排序分配失败了。标题与作者各使用阶段局部的SortKey[bookCount]分配每书 14 字节格式上限 4096 本书时为 57,344 字节第一块数组释放后才请求第二块。因此排序在完整格式上限内是尽力而为而不是在某个任意库大小处直接禁用。DEDUP_DEGRADED某个目录超过了固定的 1024 条目重复键缓冲区或它的可失败 8 KiB 分配失败。遍历仍然索引每一个枚举到的书只是停止记忆额外的身份用于重复目录项检测——所以损坏的 FAT 可能暴露重复项但绝不会让真实的书消失。selfSize是预期文件大小与真实大小比较是一次免费的截断防护被断电打断的构建不可能通过校验。打开时的严格校验validateHeaderStructure()LibraryFormat.h在只读了一个扇区的条件下完成全部结构校验magic 必须是CLX1、格式版本必须等于 2、bookCount不得超过CLIX_MAX_RECORDS 4096保证所有计数与排列序号可用 uint16_t 表示、metadataEnabled不得大于 1、真实文件大小必须等于selfSize、各 section 偏移必须与layoutSections()重算的结果完全一致。随后validateHeader()再检查 fold 版本。拒绝原因被ClixValidity枚举显式记录BadMagic / UnknownFormatVersion / StaleFoldVersion / SizeMismatch / CountOutOfRange / SectionsInconsistent并写入日志——这样由格式 bug 引起的重建循环会显示在日志中而不是表现为一次缓慢的首次启动。读取端 LibraryIndexFile.cpp 还有一层防御readRecord()在记录进入程序的唯一入口处钳制foldLen/authorKeyLen到字段容量、拒绝非法metadataStatus、并拒绝接近 u32 顶端的nameOff防止伪造值加长度后回绕通过边界检查——因为 SD 卡上的数据可能被用户写入或自然腐坏防御必须集中在单点而非散落各调用处。测试验证CLX1 布局是纯宿主机可测的LibraryFormat.h刻意不依赖 HalStorage 与 Arduinotest/library_format/LibraryFormatTest.cpp 覆盖了关键不变量结构体大小冻结sizeof(ClixHeader) 64、sizeof(ClixRecord) 128、sizeof(ClixFolderHeader) 1、CLIX_FORMAT_VERSION 2记录精确铺满扇区4096 % 128 0、4096 / 128 32、CLIX_ALIGN % sizeof(ClixRecord) 0每个区段都落在 512 边界且互不重叠偏移算术有序recordOffset(h, k) recordStart 128k每第 4 条记录天然落在扇区边界排列数组不重叠、selfSize nameStart nameLen。这些测试把磁盘契约固化为编译器可执行的断言防止任何平台相关的结构体填充悄悄改变格式。总结缓存体系的共同设计哲学综合三类文件可以提炼出 crosspoint-reader 缓存格式的几个贯穿性设计原则版本即迁移无论是book.bin的 v10、section.bin的 v47还是 CLX1 的 v2/fold v3旧版本一律拒绝加载并重建——不写迁移代码用重建吸收一切格式变化头信息自洽book.bin用lutOffset定位 LUTsection.bin的头部字段即缓存失效键CLX1 用layoutSections()由计数推导全部偏移、用selfSize做截断防护固定步长与对齐CLX1 的 128 字节记录、512 字节区段对齐、32 记录/4KB 缓冲以及 TextBlock 的扁平词存储共同目标都是一次分配 一次 SD 读在 ESP32 的有限 RAM 与 SD 卡吞吐限制下榨取性能缓存不变量由测试与 static_assert 锁死防止格式漂移。对于希望逆向调试固件缓存、或为 crosspoint-reader 贡献代码的开发者本文给出的两份 ImHex 模式可以直接粘贴使用版本演进清单则是排查为何我的章节缓存总在重建的第一手线索——答案通常就是当前阅读设置与缓存头部快照不一致或者你的 EPUB 触发了某个新版本的排版行为变化。赞分享【免费下载链接】crosspoint-readerOpen-source e-reader firmware项目地址https://gitcode.com/gh_mirrors/cr/crosspoint-reader点击查看免费下载相关推荐如何在 Scalar API Reference 中渲染 AsyncAPI 文档并理解 1.x/2.x 文档的自动升级如何在 Scalar API Reference 中渲染 AsyncAPI 文档并理解 1.x/2.x 文档的自动升级 假设你已经有一份 AsyncAPI 文档CrossPoint Reader 开发指南面向 ESP32-C3 嵌入式固件的内存约束、架构规范与调试实践CrossPoint Reader 开发指南面向 ESP32 C3 嵌入式固件的内存约束、架构规范与调试实践 导读 CrossPoint Reader 是一款CrossPoint Reader 按需 Agent Skills面向 ESP32 固件开发的工程纪律体系CrossPoint Reader 按需 Agent Skills面向 ESP32 固件开发的工程纪律体系 本篇技术指南围绕 CrossPoint Reade上一篇如何用DDrawCompat让经典Windows游戏在现代系统上流畅运行终极兼容性解决方案指南下一篇三步搞定B站缓存视频永久保存m4s-converter完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。