Flutter三方库适配OpenHarmony【doc_text】— OLE2Parser 类完整实现解析
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net上一篇讲了 OLE2 格式的概念,这篇看代码——OLE2Parser 类的每一个方法。200 行代码实现了一个能读取 Word 文档的 OLE2 解析器,虽然不完整(只实现了读取所需的最小子集),但足以应对绝大多数 .doc 文件。构造即解析:constructor 中调用 parse()
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
上一篇讲了 OLE2 格式的概念,这篇看代码——OLE2Parser 类的每一个方法。200 行代码实现了一个能读取 Word 文档的 OLE2 解析器,虽然不完整(只实现了读取所需的最小子集),但足以应对绝大多数 .doc 文件。
一、OLE2Parser 类结构
1.1 类声明与成员变量
class OLE2Parser {
private bytes: Uint8Array;
private sectorSize: number = 512;
private miniSectorSize: number = 64;
private miniStreamCutoff: number = 4096;
private firstDirSector: number = 0;
private fat: number[] = [];
private miniFat: number[] = [];
private miniStream: Uint8Array | null = null;
constructor(bytes: Uint8Array) {
this.bytes = bytes;
this.parse();
}
// ...
}
1.2 成员变量清单
| 变量 | 类型 | 默认值 | 来源 |
|---|---|---|---|
| bytes | Uint8Array | 构造参数 | 原始文件数据 |
| sectorSize | number | 512 | 头部 sectorShift |
| miniSectorSize | number | 64 | 头部 miniShift |
| miniStreamCutoff | number | 4096 | 头部偏移 0x38 |
| firstDirSector | number | 0 | 头部偏移 0x30 |
| fat | number[] | [] | readFAT 构建 |
| miniFat | number[] | [] | readMiniFAT 构建 |
| miniStream | Uint8Array | null | null | readMiniStream 构建 |
1.3 方法清单
| 方法 | 访问级别 | 作用 |
|---|---|---|
| constructor | public | 初始化并解析 |
| parse | private | 解析头部、构建 FAT |
| readU16 | private | 读取 16 位无符号整数 |
| readU32 | private | 读取 32 位无符号整数 |
| sectorOffset | private | 扇区号转文件偏移 |
| readFAT | private | 构建 FAT 表 |
| readMiniFAT | private | 构建 Mini FAT 表 |
| readMiniStream | private | 读取 Mini Stream |
| readStream | private | 读取扇区链数据 |
| parseDirectoryEntry | private | 解析目录条目 |
| findEntry | public | 按名称查找目录条目 |
| readEntryData | public | 读取条目数据 |
💡 只有 findEntry 和 readEntryData 是 public 的——这是 OLE2Parser 暴露给外部的唯一接口。外部代码不需要知道 FAT、扇区这些内部概念。
二、构造函数与 parse() 初始化
2.1 构造函数
constructor(bytes: Uint8Array) {
this.bytes = bytes;
this.parse();
}
构造函数做了两件事:保存原始数据,立即解析。这意味着 OLE2Parser 一旦创建就是"就绪"状态。
2.2 parse() 完整实现
private parse(): void {
const sectorShift = this.readU16(30);
const miniShift = this.readU16(32);
this.sectorSize = 1 << sectorShift;
this.miniSectorSize = 1 << miniShift;
this.miniStreamCutoff = this.readU32(56);
this.firstDirSector = this.readU32(48);
const numFatSectors = this.readU32(44);
this.readFAT(numFatSectors);
const firstMiniFatSector = this.readU32(60);
if (firstMiniFatSector !== 0xFFFFFFFE) {
this.readMiniFAT(firstMiniFatSector);
}
this.readMiniStream();
}
2.3 初始化依赖链
parse()
├── 1. 读取头部参数(sectorSize, firstDirSector 等)
│ ↓ 依赖头部参数
├── 2. readFAT() → 构建 FAT 表
│ ↓ 依赖 FAT 表
├── 3. readMiniFAT() → 构建 Mini FAT 表
│ ↓ 依赖 FAT 表 + 目录
└── 4. readMiniStream() → 读取 Mini Stream
三、readFAT 详解
3.1 完整实现
private readFAT(numSectors: number): void {
this.fat = [];
for (let i = 0; i < Math.min(numSectors, 109); i++) {
const fatSector = this.readU32(76 + i * 4);
if (fatSector === 0xFFFFFFFF || fatSector === 0xFFFFFFFE) break;
const offset = this.sectorOffset(fatSector);
const entriesPerSector = this.sectorSize / 4;
for (let j = 0; j < entriesPerSector; j++) {
const entryOffset = offset + j * 4;
if (entryOffset + 4 <= this.bytes.length) {
this.fat.push(this.readU32(entryOffset));
}
}
}
}
3.2 逐行解析
| 行 | 代码 | 说明 |
|---|---|---|
| 1 | Math.min(numSectors, 109) |
头部最多存 109 个 DIFAT 条目 |
| 2 | this.readU32(76 + i * 4) |
从偏移 0x4C 开始读 DIFAT |
| 3 | fatSector === 0xFFFFFFFF |
空闲标记,停止 |
| 4 | this.sectorOffset(fatSector) |
FAT 扇区的文件偏移 |
| 5 | this.sectorSize / 4 |
每个扇区有多少个 FAT 条目 |
| 6 | this.fat.push(...) |
把 FAT 条目加入数组 |
3.3 FAT 构建过程示例
假设 numFatSectors = 2,sectorSize = 512
DIFAT[0] = readU32(76) = 0 → FAT 扇区 0
DIFAT[1] = readU32(80) = 3 → FAT 扇区 3
FAT 扇区 0(偏移 512):128 个条目
FAT 扇区 3(偏移 2048):128 个条目
总共 256 个 FAT 条目 → 可以管理 256 个扇区
四、readMiniFAT 与 readMiniStream
4.1 readMiniFAT
private readMiniFAT(startSector: number): void {
const data = this.readStream(startSector, this.sectorSize * 10, false);
if (!data) return;
this.miniFat = [];
for (let i = 0; i < data.length - 3; i += 4) {
this.miniFat.push(
(data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24)) >>> 0
);
}
}
4.2 为什么读 sectorSize * 10
const data = this.readStream(startSector, this.sectorSize * 10, false);
// 512 * 10 = 5120 字节
// 5120 / 4 = 1280 个 Mini FAT 条目
// 1280 * 64 = 81920 字节的迷你流空间
这是一个估算值——假设 Mini FAT 不会超过 10 个扇区。对于大多数 Word 文档来说足够了。
4.3 readMiniStream
private readMiniStream(): void {
const dirData = this.readStream(this.firstDirSector, this.sectorSize, false);
if (!dirData) return;
const entry = this.parseDirectoryEntry(dirData, 0);
if (entry.type === 5 && entry.size > 0) {
this.miniStream = this.readStream(entry.startSector, entry.size, false);
}
}
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 读取目录流的第一个扇区 | 获取 Root Entry |
| 2 | 解析第 0 个目录条目 | Root Entry 总是索引 0 |
| 3 | 检查 type === 5 | 确认是 Root Entry |
| 4 | 读取 Root Entry 的数据 | 这就是 Mini Stream |
📌 Mini Stream 存储在 Root Entry 的流数据中。这是 OLE2 规范的设计——Root Entry 既是目录的根节点,也是 Mini Stream 的容器。
五、readStream 双模式实现
5.1 核心算法
private readStream(startSector: number, size: number, useMini: boolean): Uint8Array | null {
if (size <= 0) return null;
const result = new Uint8Array(size);
let bytesRead = 0;
let current = startSector;
if (useMini && this.miniStream) {
while (current !== 0xFFFFFFFE && bytesRead < size) {
const offset = current * this.miniSectorSize;
const toRead = Math.min(this.miniSectorSize, size - bytesRead);
if (offset + toRead <= this.miniStream.length) {
result.set(this.miniStream.subarray(offset, offset + toRead), bytesRead);
bytesRead += toRead;
}
if (current < this.miniFat.length) {
current = this.miniFat[current];
} else {
break;
}
}
} else {
while (current !== 0xFFFFFFFE && bytesRead < size) {
const offset = this.sectorOffset(current);
const toRead = Math.min(this.sectorSize, size - bytesRead);
if (offset + toRead <= this.bytes.length) {
result.set(this.bytes.subarray(offset, offset + toRead), bytesRead);
bytesRead += toRead;
}
if (current < this.fat.length) {
current = this.fat[current];
} else {
break;
}
}
}
return result;
}
5.2 防御性检查
| 检查 | 代码 | 防御的问题 |
|---|---|---|
| 空流 | if (size <= 0) return null |
无效的流大小 |
| 越界 | offset + toRead <= bytes.length |
扇区超出文件范围 |
| FAT 越界 | current < fat.length |
FAT 表不完整 |
| 链结束 | current !== 0xFFFFFFFE |
正常的链结束 |
| 读满 | bytesRead < size |
已读够需要的字节 |
六、sectorOffset 方法
6.1 实现
private sectorOffset(sector: number): number {
return 512 + sector * this.sectorSize;
}
6.2 硬编码的 512
// 头部固定占 512 字节,不受 sectorSize 影响
return 512 + sector * this.sectorSize;
// ^^^
// 头部大小(固定值)
即使 sectorSize 是 4096(大扇区模式),头部仍然是 512 字节。这是 OLE2 规范的规定。
七、readU16 和 readU32
7.1 实现
private readU16(offset: number): number {
if (offset + 1 >= this.bytes.length) return 0;
return this.bytes[offset] | (this.bytes[offset + 1] << 8);
}
private readU32(offset: number): number {
if (offset + 3 >= this.bytes.length) return 0;
return (this.bytes[offset] |
(this.bytes[offset + 1] << 8) |
(this.bytes[offset + 2] << 16) |
(this.bytes[offset + 3] << 24)) >>> 0;
}
7.2 边界保护
if (offset + 1 >= this.bytes.length) return 0;
if (offset + 3 >= this.bytes.length) return 0;
如果偏移超出文件范围,返回 0 而不是崩溃。这在处理损坏文件时很重要。
7.3 DocTextPlugin 中也有同名方法
// OLE2Parser 中的 readU16/readU32
private readU16(offset: number): number { ... }
// DocTextPlugin 中也有 readU16/readU32
private readU16(bytes: Uint8Array, offset: number): number { ... }
| 区别 | OLE2Parser | DocTextPlugin |
|---|---|---|
| 数据源 | this.bytes(隐式) | bytes 参数(显式) |
| 作用域 | OLE2Parser 内部 | DocTextPlugin 内部 |
两个类各自有自己的 readU16/readU32,因为它们操作的数据源不同。
八、公开接口:findEntry 和 readEntryData
8.1 findEntry
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;
}
8.2 readEntryData
readEntryData(entry: DirectoryEntry): Uint8Array | null {
const useMini = entry.size < this.miniStreamCutoff;
return this.readStream(entry.startSector, entry.size, useMini);
}
8.3 使用示例
const ole = new OLE2Parser(bytes);
// 查找 WordDocument 流
const wordEntry = ole.findEntry("WordDocument");
if (!wordEntry) return null;
// 读取流数据
const wordData = ole.readEntryData(wordEntry);
💡 readEntryData 自动判断使用普通扇区还是迷你扇区——通过
entry.size < this.miniStreamCutoff(默认 4096)来决定。调用者不需要关心这个细节。
总结
OLE2Parser 用 200 行代码实现了 OLE2 格式的核心解析:
- 构造即解析:constructor 中调用 parse() 完成初始化
- FAT 构建:从 DIFAT 数组读取 FAT 扇区,构建完整 FAT 表
- 双模式流读取:普通扇区(≥4096B)和迷你扇区(<4096B)
- 公开接口:findEntry + readEntryData,屏蔽内部复杂性
- 防御性编程:边界检查、越界返回 0、链断裂保护
下一篇我们看目录条目解析——怎么从 128 字节的二进制数据中提取出流的名称、类型和位置。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- MS-CFB 规范
- doc_text Gitcode 仓库
- Uint8Array.subarray
- JavaScript 位运算
- TypedArray.set
- Apache POI OLE2 实现
- 小端序读取
- 开源鸿蒙跨平台社区

OLE2Parser 内部结构与数据流
更多推荐



所有评论(0)