Flutter三方库适配OpenHarmony【doc_text】— 目录条目解析与 WordDocument 流定位
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.netOLE2 的目录流就像一个"文件目录"——记录了每个流的名称、类型、起始扇区和大小。doc_text 需要从中找到流(文本数据)和流(格式信息)。这篇把 parseDirectoryEntry 和 findEntry 的每一行代码都过一遍。目录条目(128 字节):偏移 长度 字段0x00
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
OLE2 的目录流就像一个"文件目录"——记录了每个流的名称、类型、起始扇区和大小。doc_text 需要从中找到 WordDocument 流(文本数据)和 1Table/0Table 流(格式信息)。这篇把 parseDirectoryEntry 和 findEntry 的每一行代码都过一遍。
一、目录条目的 128 字节布局
1.1 结构定义
目录条目(128 字节):
偏移 长度 字段
0x00 64 名称(UTF-16LE,最长 32 字符)
0x40 2 名称长度(字节数,包含终止符)
0x42 1 条目类型
0x43 1 颜色(红黑树:0=红,1=黑)
0x44 4 左兄弟 ID
0x48 4 右兄弟 ID
0x4C 4 子节点 ID
0x50 16 CLSID
0x60 4 状态位
0x64 8 创建时间
0x6C 8 修改时间
0x74 4 起始扇区
0x78 4 流大小
0x7C 4 保留
1.2 DirectoryEntry 接口
interface DirectoryEntry {
name: string;
type: number;
startSector: number;
size: number;
}
doc_text 只提取了四个字段——名称、类型、起始扇区、大小。其他字段(颜色、兄弟节点、时间戳等)对于文本提取不需要。
1.3 条目类型
| type 值 | 含义 | 说明 |
|---|---|---|
| 0 | Empty | 空条目(目录结束标记) |
| 1 | Storage | 存储(类似文件夹) |
| 2 | Stream | 流(类似文件) |
| 5 | Root Entry | 根条目(特殊) |
二、parseDirectoryEntry 完整实现
2.1 源码
private parseDirectoryEntry(data: Uint8Array, index: number): DirectoryEntry {
const offset = index * 128;
if (offset + 128 > data.length) {
return { name: "", type: 0, startSector: 0, size: 0 };
}
// 读取名称
const nameLen = data[offset + 64] | (data[offset + 65] << 8);
let name = "";
if (nameLen > 0 && nameLen <= 64) {
for (let i = 0; i < Math.min(nameLen - 2, 62); i += 2) {
const ch = data[offset + i] | (data[offset + i + 1] << 8);
if (ch > 0 && ch < 128) {
name += String.fromCharCode(ch);
}
}
}
const type = data[offset + 66];
const startSector = (data[offset + 116] |
(data[offset + 117] << 8) |
(data[offset + 118] << 16) |
(data[offset + 119] << 24)) >>> 0;
const size = (data[offset + 120] |
(data[offset + 121] << 8) |
(data[offset + 122] << 16) |
(data[offset + 123] << 24)) >>> 0;
return { name, type, startSector, size };
}
2.2 逐段解析
| 段落 | 代码 | 说明 |
|---|---|---|
| 偏移计算 | index * 128 |
每个条目 128 字节 |
| 边界检查 | offset + 128 > data.length |
防止越界 |
| 名称长度 | data[offset + 64] | (data[offset + 65] << 8) |
偏移 0x40,2 字节 |
| 名称解析 | UTF-16LE 逐字符读取 | 只取 ASCII 部分 |
| 类型 | data[offset + 66] |
偏移 0x42,1 字节 |
| 起始扇区 | 偏移 0x74(116),4 字节 | 小端序 |
| 大小 | 偏移 0x78(120),4 字节 | 小端序 |
三、Unicode 名称解析
3.1 UTF-16LE 编码
"WordDocument" 在 UTF-16LE 中的存储:
W o r d D o c u m e n t \0
57 00 6F 00 72 00 64 00 44 00 6F 00 63 00 75 00 6D 00 65 00 6E 00 74 00 00 00
每个字符占 2 个字节,低字节在前(小端序)。
3.2 名称读取代码
const nameLen = data[offset + 64] | (data[offset + 65] << 8);
let name = "";
if (nameLen > 0 && nameLen <= 64) {
for (let i = 0; i < Math.min(nameLen - 2, 62); i += 2) {
const ch = data[offset + i] | (data[offset + i + 1] << 8);
if (ch > 0 && ch < 128) {
name += String.fromCharCode(ch);
}
}
}
3.3 关键细节
| 细节 | 代码 | 说明 |
|---|---|---|
| nameLen - 2 | Math.min(nameLen - 2, 62) |
减去终止符的 2 字节 |
| i += 2 | 步长为 2 | UTF-16LE 每字符 2 字节 |
| ch < 128 | if (ch > 0 && ch < 128) |
只取 ASCII 字符 |
| 62 上限 | Math.min(..., 62) |
名称区域最大 64 字节 - 2 |
3.4 只取 ASCII 的限制
if (ch > 0 && ch < 128) {
name += String.fromCharCode(ch);
}
这意味着非 ASCII 名称(比如中文流名称)会被忽略。但 Word 文档中的关键流名称都是 ASCII 的:
| 流名称 | 全 ASCII | 能否识别 |
|---|---|---|
| WordDocument | ✅ | ✅ |
| 1Table | ✅ | ✅ |
| 0Table | ✅ | ✅ |
| Data | ✅ | ✅ |
| CompObj | ✅ | ✅ |
| Root Entry | ✅ | ✅ |
📌 Word 文档的所有关键流名称都是 ASCII,所以只取 ASCII 字符不影响功能。如果需要支持其他 OLE2 文件(比如中文命名的 Excel 工作表),就需要完整的 UTF-16 解码。
四、findEntry 方法
4.1 完整实现
findEntry(name: string): DirectoryEntry | null {
const dirData = this.readStream(this.firstDirSector, this.sectorSize * 10, false);
if (!dirData) return null;
for (let i = 0; i < 100; i++) {
const entry = this.parseDirectoryEntry(dirData, i);
if (entry.type === 0) break;
if (entry.name === name) {
return entry;
}
}
return null;
}
4.2 执行流程
findEntry("WordDocument")
↓
readStream(firstDirSector, 5120, false) → 读取目录流数据
↓
for i = 0 to 99:
parseDirectoryEntry(dirData, i) → 解析第 i 个条目
if type === 0 → 空条目,结束遍历
if name === "WordDocument" → 找到了!返回
↓
return null → 没找到
4.3 为什么读 sectorSize * 10
const dirData = this.readStream(this.firstDirSector, this.sectorSize * 10, false);
// 512 * 10 = 5120 字节
// 5120 / 128 = 40 个目录条目
大多数 Word 文档的目录条目不超过 40 个。读 10 个扇区是一个安全的估算。
4.4 为什么遍历上限是 100
for (let i = 0; i < 100; i++) {
这是另一个安全上限。即使目录数据很大,也不会遍历超过 100 个条目。实际上 type === 0 的检查会更早终止循环。
4.5 线性遍历 vs 红黑树
OLE2 规范中,目录条目组织成红黑树结构:
每个条目有 leftSibling、rightSibling、childID 字段
doc_text 的做法:忽略树结构,线性遍历所有条目
| 方案 | 优点 | 缺点 |
|---|---|---|
| 线性遍历 | 简单、代码少 | O(n) 复杂度 |
| 红黑树遍历 | O(log n) 复杂度 | 实现复杂 |
💡 对于 Word 文档来说,目录条目通常不超过 20 个。线性遍历的 O(n) 和红黑树的 O(log n) 差异可以忽略不计。简单实现更重要。
五、readEntryData 方法
5.1 实现
readEntryData(entry: DirectoryEntry): Uint8Array | null {
const useMini = entry.size < this.miniStreamCutoff;
return this.readStream(entry.startSector, entry.size, useMini);
}
5.2 自动选择读取模式
entry.size < 4096 → useMini = true → 从 Mini Stream 读取
entry.size >= 4096 → useMini = false → 从普通扇区读取
5.3 使用示例
// 读取 WordDocument 流(通常很大,用普通扇区)
const wordEntry = ole.findEntry("WordDocument");
const wordData = ole.readEntryData(wordEntry);
// useMini = false(WordDocument 通常 > 4096 字节)
// 读取 CompObj 流(通常很小,用迷你扇区)
const compEntry = ole.findEntry("CompObj");
const compData = ole.readEntryData(compEntry);
// useMini = true(CompObj 通常 < 4096 字节)
六、Word 文档中的关键流
6.1 doc_text 需要的流
// 1. WordDocument 流 — 必须
const wordEntry = ole.findEntry("WordDocument");
const wordData = ole.readEntryData(wordEntry);
// 2. 1Table 或 0Table 流 — 需要(Piece Table 在这里)
let tableEntry = ole.findEntry("1Table");
if (!tableEntry) {
tableEntry = ole.findEntry("0Table");
}
const tableData = ole.readEntryData(tableEntry);
6.2 流的用途
| 流名称 | 内容 | doc_text 是否使用 |
|---|---|---|
| WordDocument | FIB + 文本数据 | ✅ 必须 |
| 1Table | 格式信息(Piece Table) | ✅ 需要 |
| 0Table | 备用格式信息 | ✅ 备选 |
| Data | 嵌入对象 | ❌ |
| CompObj | 复合对象信息 | ❌ |
| SummaryInformation | 文档属性 | ❌ |
6.3 1Table vs 0Table
// 优先使用 1Table
let tableEntry = ole.findEntry("1Table");
if (!tableEntry) {
tableEntry = ole.findEntry("0Table");
}
| 流 | 使用场景 |
|---|---|
| 1Table | Word 97+ 默认使用 |
| 0Table | 旧版本或特殊情况 |
Word 文档的 FIB 中有一个标志位指示应该使用哪个 Table 流。doc_text 简化了这个逻辑——先试 1Table,不行再试 0Table。
七、边界检查与错误处理
7.1 parseDirectoryEntry 的边界检查
if (offset + 128 > data.length) {
return { name: "", type: 0, startSector: 0, size: 0 };
}
如果数据不够 128 字节,返回一个空条目(type=0)。findEntry 中的 if (entry.type === 0) break 会终止遍历。
7.2 findEntry 的空值处理
const dirData = this.readStream(this.firstDirSector, this.sectorSize * 10, false);
if (!dirData) return null;
如果目录流读取失败,直接返回 null。
7.3 调用链中的空值传播
const wordEntry = ole.findEntry("WordDocument");
if (!wordEntry) {
return null; // 找不到 WordDocument 流
}
const wordData = ole.readEntryData(wordEntry);
if (!wordData) {
return null; // 读取流数据失败
}
每一步都检查 null,确保不会在后续操作中崩溃。
八、完整的流定位流程
8.1 从文件到文本数据
.doc 文件(Uint8Array)
↓
OLE2Parser(bytes) → 解析头部、构建 FAT
↓
findEntry("WordDocument") → 遍历目录,找到条目
↓
readEntryData(wordEntry) → 根据 FAT 链读取流数据
↓
WordDocument 流数据(Uint8Array)
↓
extractWordText(wordData, ole) → 解析 FIB + Piece Table
↓
纯文本字符串
8.2 涉及的数据结构
| 数据结构 | 类型 | 作用 |
|---|---|---|
| bytes | Uint8Array | 原始文件 |
| fat | number[] | 扇区链表 |
| miniFat | number[] | 迷你扇区链表 |
| miniStream | Uint8Array | 迷你流数据 |
| dirData | Uint8Array | 目录流数据 |
| DirectoryEntry | interface | 目录条目信息 |
| wordData | Uint8Array | WordDocument 流 |
| tableData | Uint8Array | Table 流 |
总结
目录条目解析是连接 OLE2 底层结构和 Word 文档内容的桥梁:
- 128 字节条目:名称(UTF-16LE)+ 类型 + 起始扇区 + 大小
- 线性遍历:忽略红黑树结构,简单有效
- 自动模式选择:readEntryData 根据大小决定普通/迷你扇区
- 关键流:WordDocument(文本)+ 1Table/0Table(格式)
- 防御性编程:每一步都检查 null 和边界
下一篇我们进入 Word 文档的核心——FIB 解析与 Piece Table 文本提取。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- MS-CFB 目录条目规范
- MS-DOC WordDocument 流
- UTF-16LE 编码
- doc_text Gitcode 仓库
- 红黑树
- String.fromCharCode
- Apache POI DirectoryEntry
- 开源鸿蒙跨平台社区

OLE2 目录流与流定位
更多推荐



所有评论(0)