前言

欢迎加入开源鸿蒙跨平台社区: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 格式的核心解析:

  1. 构造即解析:constructor 中调用 parse() 完成初始化
  2. FAT 构建:从 DIFAT 数组读取 FAT 扇区,构建完整 FAT 表
  3. 双模式流读取:普通扇区(≥4096B)和迷你扇区(<4096B)
  4. 公开接口:findEntry + readEntryData,屏蔽内部复杂性
  5. 防御性编程:边界检查、越界返回 0、链断裂保护

下一篇我们看目录条目解析——怎么从 128 字节的二进制数据中提取出流的名称、类型和位置。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

OLE2Parser 架构
OLE2Parser 内部结构与数据流

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐