复合二进制文档 - 文档结构提取(中篇)

使用JS API 解析复合二进制文档

系列文章分为上中下三篇,教你怎么使用纯js解析复合二进制文档(doc,ppt,msi)

关于复合二进制文档

前文已经大致讲解了符合二进制文档的背景已经二进制结构;根据其二进制结构,我们是能解析并提取一些我们感兴趣的数据的;

解析

准备工作1

假定我们在浏览器使用input标签上传了一个复合二进制文档dox,msi类型均可,此时拿到的是Blob数据格式,为了更好的读取二进制信息,需要一个工具函数将Blob转换成ArrayBuffer;

js 复制代码
function readBlob(blob: Blob): Promise<ArrayBuffer> {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(blob);
    return new Promise<ArrayBuffer>(resolve => {
        fileReader.onload = (event) => {
            if (!event.target || event.target.readyState !== FileReader.prototype.DONE) {
                throw new Error("Read data failed");
            }
            const array = event.target.result as ArrayBuffer;
            resolve(array);
        };
    });
}

函数逻辑就不细说了,前端程序员基本操作。

准备工作2

文档头和扇区都是小块二进制数据,这样避免了一次性读取整个文档;因此还需要一个读个扇区或者二进制块的工具函数

js 复制代码
// 读取某个扇区的二进制数据
async function readSector(sectorNumber: number): Promise<ArrayBuffer> {
        const offset = (sectorNumber + 1) * sectorSize;
        const sector = await this.readPartOfFile(offset, sectorSize);
        return sector;
    }

// 读取某个二进制代码块
async function readPartOfFile(offset: number, count: number): Promise<ArrayBuffer> {
        const blob = file.slice(offset, offset + count);
        const result = await readBlob(blob);
        return result;
    }
读取文档头

文档头是固定512个字节,详细结构如下:

偏移量(Offset) 大小(Size) 内容(Contents)
0 8 复合文档文件标识:D0H CFH 11H E0H A1H B1H 1AH E1H
8 16 此文件的唯一标识(不重要, 可全部为0)
24 2 文件格式修订号 (一般为003EH)
26 2 文件格式版本号(一般为0003H)
28 2 字节顺序规则标识(大端与小端):FEH FFH = Little-Endian,FFH FEH = Big-Endian
30 2 复合文档中sector的大小(ssz),以2的幂形式存储
32 2 short-sector的大小,以2的幂形式存储
34 10 Not used
44 4 用于存放扇区配置表(SAT)的sector总数
48 4 用于存放目录流的第一个sector的SID
52 4 Not used
56 4 标准流的最小大小(一般为4096 bytes),小于此值的流即为短流。
60 4 用于存放短扇区配置表(SSAT)的第一个sector的SID,或为--2 (End Of Chain SID)如不存在。
64 4 用于存放短扇区配置表(SSAT)的sector总数
68 4 用于存放主扇区配置表(MSAT)的第一个sector的SID,或为--2 (End Of Chain SID) 若无附加的sectors。
72 4 用于存放主扇区配置表(MSAT)的sector总数
76 436 存放主扇区配置表(MSAT)的第一部分,包含109个SID。

在读取文档头内容时,核心关注:扇区大小,短扇区大小,扇区配置表总数,存档目录的第一个扇区标识,标准流大小,用于存放短扇区配置表的总数,用于存放短扇区配置表的第一个扇区扇区标识,用于存放主扇区配置表的第一个扇区标识,以及主扇区配置表的第前109个扇区标识;

特别注意的是:扇区标识是一个32为的有符号整形,占据4个字节。

js 复制代码
    const NUMBER_OF_DIFAT_IN_HEADER = 109;
    const SIGNATURE = [208, 207, 17, 224, 161, 177, 26, 225];
    const SECTOR_SIZE_OFFSET = 30;
    const MINI_SECTOR_SIZE_OFFSET = 32;
    const NUMBER_OF_DIRECTORY_SECTORS_OFFSET = 40;
    const NUMBER_OF_FAT_SECTORS_OFFSET = 44;
    const FIRST_DIRECTORY_SECTOR_OFFSET = 48;
    const MINI_STREAM_CUTOFF_SIZE_OFFSET = 56;
    const FIRST_MINI_FATSECTOR_OFFSET = 60;
    const NUMBER_OF_MINI_FATSECTORS_OFFSET = 64;
    const FIRST_DIFAT_SECTOR_OFFSET = 68;
    const NUMBER_OF_DIFAT_SECTORS_OFFSET = 72;
    const DIFAT_TABLE_OFFSET = 76;
    const HEADER_SIZE = DIFAT_TABLE_OFFSET + NUMBER_OF_DIFAT_IN_HEADER * 4;

     async function readHeaders() {
        const header = await readPartOfFile(0, HEADER_SIZE);
        const signature = new Uint8Array(header.slice(0, 8));
        SIGNATURE.forEach(
        );
        const data = new DataView(header, 0, header.byteLength);
        const sectorSize= Math.pow(2, data.getUint16(SECTOR_SIZE_OFFSET, true)); // 扇区大小
        const miniSectorSize= Math.pow(2, data.getUint16(MINI_SECTOR_SIZE_OFFSET, true)); // 短扇区大小
        const numberOfDirectorySectors= data.getInt32(NUMBER_OF_DIRECTORY_SECTORS_OFFSET, true); // 目录扇区数量
        const numberOfFatSectors= data.getInt32(NUMBER_OF_FAT_SECTORS_OFFSET, true); // 扇区配置扇区数量
        const firstDirectorySector= data.getInt32(FIRST_DIRECTORY_SECTOR_OFFSET, true); // 第一个目录扇区标识
        const miniStreamCutoffSize= data.getInt32(MINI_STREAM_CUTOFF_SIZE_OFFSET, true); // 短流的临界值
        const firstMiniFatSector= data.getInt32(FIRST_MINI_FATSECTOR_OFFSET, true); // 第一个短扇区的标识
        const numberOfMiniFatSectors= data.getInt32(NUMBER_OF_MINI_FATSECTORS_OFFSET, true); // 短扇区配置表的扇区数量
        const firstDifatSector= data.getInt32(FIRST_DIFAT_SECTOR_OFFSET, true); // 第一个主扇区配置表的扇区标识
        const numberOfDifatSectors= data.getInt32(NUMBER_OF_DIFAT_SECTORS_OFFSET, true); // 主扇区配置表的扇区数量
        const countOfMiniSectorsInSector = sectorSize / miniSectorSize; // 一个标准扇区里有多少个短扇区
        const difatTable = []; // 主扇区配置表
        // 主扇区前109个扇区配置表表示
        for (let i = 0; i < NUMBER_OF_DIFAT_IN_HEADER; i++) {
            const offset = DIFAT_TABLE_OFFSET + i * 4;
            const fatSectorNumber = data.getInt32(offset, true);
            difatTable.push(fatSectorNumber);
        }
    }
读取非文档头里的主扇区配置表

文档头中已经指明了用于存放MSAT的第一个sector的SID --> firstDifatSector; 主扇区配置表扇区的结构如下:(假定扇区大小为 sectorSize 字节,则可以存放(n-4/4)个SID),那么:

offset size content
0 sectorSize-4 SID数组(数量为(sectorSize-4)/4)
sectorSize-4 4 下一个扇区的SID

实现代码:

js 复制代码
   async function readDifats() {
        let difatSector =firstDifatSector; // 第一个存放主扇区配置表的扇区标识
        for (let sectorsCount = 0; sectorsCount < numberOfDifatSectors; sectorsCount++) {
            if (difatSector === END_OF_CHAIN) {
               return; // 如果第一个扇区标识是 ND_OF_CHAIN ,则表示没主扇区配置存放在扇区里面(全部存放在文档头中)
            }
            const sector = await readSector(difatSector);
            const data = new DataView(sector, 0, sector.byteLength);
            const countOfEntriesInSector = (sectorSize - 4) / 4; // 最后四个字节记录扇区链下一个扇区的标识
            for (let i = 0; i < countOfEntriesInSector; i++) {
                const offset = i * 4;
                const fatSectorNumber = data.getInt32(offset, true);
                difatTable.push(fatSectorNumber);
            }
            difatSector = data.getInt32(sectorSize - 4, true);
        }
        if (difatSector !== END_OF_CHAIN) {
            throw false // 最后一个扇区链的标识若不是结束标识,抛出异常
        }
        return difatTable;
    }
构建扇区配置表(fatTable)

扇区配置表是一个扇区标识数组,数组下标i标识当前扇区的扇区标识,下标i对应的值标识扇区链的下一个扇区的扇区标识; 扇区配置表记录在不同扇区中,记录扇区配置表的扇区的扇区标识由主扇区配置表(difatTable)记录; 我们

js 复制代码
     async function getNextSectorInChain(sectorNumber: number): Promise<number> {
        const countOfEntriesInSector = sectorSize / 4; // 每个扇区能存放扇区标识的数量
        const fatPageNumber = Math.floor(sectorNumber / countOfEntriesInSector); // 标识位于第几个存放扇区
        const offsetInFatPage = sectorNumber % countOfEntriesInSector; // 表示在存放扇区的偏移量
        if (!fatTable[fatPageNumber]) { // 是否建立该扇区的配置表记录
            const fatSector = await readFatSector(fatPageNumber); 
            fatTable[fatPageNumber] = fatSector;
        }
        return fatTable[fatPageNumber][offsetInFatPage]; // 扇区配置表数组中,index表示当前扇区,array[index]表示当前扇区的下一个扇区
    }

    // 获取某个保存了扇区配置表的扇区的内容
    // 入参 fatPageNumber 表示第几个 保存扇区配置表的扇区
     async function readFatSector(fatPageNumber: number): Promise<number[]> {
        const fatSectorNumber = difatTable[fatPageNumber]; // 在主扇区配置表中拿到保存扇区配置表的扇区的扇区标识
        const sector = await readSector(fatSectorNumber);
        const data = new DataView(sector, 0, sector.byteLength);
        const fatPage: number[] = [];
        const countOfEntriesInSector = sectorSize / 4; // 计算改扇区保存了多少个扇区标识
        for (let i = 0; i < countOfEntriesInSector; i++) {
            const offset = i * 4;
            const nextSectorNumber = data.getInt32(offset, true);
            fatPage.push(nextSectorNumber);
        }
        return fatPage;
    }
短扇区扇区配置表

逻辑跟读取扇区配置表几乎相同,需要注意的是,短扇区配置表获取下一个存放扇区时,使用通用的计算下一个扇区标识的方式(getNextSectorInChain),而扇区配置表是在主扇区配置中获取;

js 复制代码
 async function readMiniFats(): Promise<void> {
        let miniFatSectorNumber = firstMiniFatSector;
        for (let sectorsCount = 0; sectorsCount < numberOfMiniFatSectors; sectorsCount++) {
            ...
            miniFatSectorNumber = await getNextSectorInChain(miniFatSectorNumber);
        }
        ...
        if (miniFatSectorNumber !== END_OF_CHAIN) {
            return
        }
    }
目录配置

一个目录入口的大小严格地为128字节,二禁止结构为:

偏移量(字节) 长度(字节) 描述
0 64 此入口的名字(字符数组),一般为16位的Unicode字符,以0结束。(最大长度为31个字符)
64 2 用于存放名字的区域的大小,包括结尾的0。(如:一个名字有5个字符则此值为(5+1)∙2 = 12)
66 1 入口类型:unknown = 0,storage = 1,stream = 2,rootStorage = 5,
67 1 此入口的节点颜色:00H = Red,01H = Black
68 4 其左节点的DID(若此入口为user storage或stream),若没有左节点就为-1。
72 4 其右节点的DID(若此入口为user storage或stream),若没有右节点就为-1。
76 4 其成员红黑树的根节点的DID(若此入口为storage),其他为-1。
80 16 唯一标识符(若为storage)(不重要,可能全为0)
96 4 用户标记(不重要,可能全为0)
100 8 创建此入口的时间标记。大多数情况都不写。
108 8 最后修改此入口的时间标记。大多数情况都不写。
116 4 若此为流的入口,指定流的第一个sector或short-sector的SID;若此为根仓库入口,指定短流存放流的第一个sector的SID;其他情况为0。
120 4 若此为流的入口,指定流的大小(字节);若此为根仓库入口,指定短流存放流的大小(字节);其他情况为0。
124 4 -----

因此,读取目录配置的代码如下:

js 复制代码
async function readDirectories(): Promise<void> {
        let directorySectorNumber = firstDirectorySector; // 文档头中标识的第一个目录扇区的扇区标识
        for (let sectorCount = 0; sectorCount < MAX_SECTORS_COUNT_IN_CHAIN; sectorCount++) {
            if (directorySectorNumber === END_OF_CHAIN) {
                break;
            }
            const sector = await readSector(directorySectorNumber);
            const data = new DataView(sector, 0, sector.byteLength);
            const countOfEntriesInSector = sectorSize / 128; // 一个扇区总目录入口数量
            for (let i = 0; i < countOfEntriesInSector; i++) {
                const offset = i * 128;
                const nameLength = data.getUint16(offset + 64, true); // 目录名称保存在一个扇区的前0-64字节
                const nameArray = sector.slice(offset, offset + nameLength);
                const name = String.fromCharCode.apply(null, Array.from(new Uint16Array(nameArray)));
                let streamSize = 0;
                if (sectorSize <= 512) {
                    streamSize = data.getInt32(offset + 120, true);
                } else {
                    streamSize = Number(data.getBigInt64(offset + 120, true));
                }
                const directory = {
                    nameArray: nameArray,
                    name: decodeMsiDirectoryName(name),
                    objectType: data.getInt8(offset + 66), // 目录类型: 流,短流,更目录
                    startSector: data.getInt32(offset + 116, true), // 流的其实扇区标识
                    streamSize: streamSize,
                    isMiniStream: streamSize < miniStreamCutoffSize, // 小于指定的大小,则是短流
                };
                if (directory.objectType === 5 || directory.objectType === 1 || directory.objectType === 2) {
                    directoryTable.push(directory);
                }
            }
            directorySectorNumber = await getNextSectorInChain(directorySectorNumber);
        }
        if (directorySectorNumber !== END_OF_CHAIN) {
            return
        }
    }
流的读取

流的读取,需要明确流的起始扇区标识和流大小,在目录结构中都有记录。 假设流的起始扇区标识为startSectorNumber,流大小为streamSize,那么流的读取逻辑如下:

js 复制代码
  async function readStream(startSectorNumber: number, streamSize: number): Promise<Uint8Array> {
        const result = new Uint8Array(streamSize);
        let sectorNumber = startSectorNumber;
        for (let sectorCount = 0; sectorCount < MAX_SECTORS_COUNT_IN_CHAIN; sectorCount++) {
            if (sectorNumber === END_OF_CHAIN) {
                break;
            }
            const sector = await readSector(sectorNumber);
            const offset = sectorCount * sectorSize; // 计算偏移量
            const dataSize = Math.min(sectorSize, streamSize - offset);
            result.set(new Uint8Array(sector.slice(0, dataSize)), offset); //按照偏移量,将每个扇区的二进制数据放入结果数组中
            sectorNumber = await getNextSectorInChain(sectorNumber);
        }
        if (sectorNumber !== END_OF_CHAIN) {
            return;
        }
        return result;
    }
短流读取

短流的读取相对复杂。短流是小于标准大小的流,他们并不是使用常规数据扇区来存储,而是集中存放在一个特殊的短流存放容器中,这个容器本身是一个常规的扇区。

短流也拥有短流扇区配置表等类似于普通流的配置。

js 复制代码
   //用途:计算迷你扇区号对应的标准扇区索引
   //示例:如果每个扇区有N个短扇区,短扇区号M对应标准扇区索引math.floor(M / N)  
    function getSectorCountByMiniSectorNumber(miniSectorNumber: number): number {
        return Math.floor(miniSectorNumber / this.fileInfo.countOfMiniSectorsInSector);
    }

    //定位并读取包含目标迷你扇区的标准扇区。
    async function readSectorByMiniSectorNumber(
        startSectorNumber: number,
        miniSectorNumber: number
    ): Promise<[ArrayBuffer, number]> {
        let sectorNumber = startSectorNumber;
        const desiredSectorCount = getSectorCountByMiniSectorNumber(miniSectorNumber);
        for (let sectorCount = 0; sectorCount < desiredSectorCount; sectorCount++) {
            if (sectorNumber === END_OF_CHAIN) {
                ...
            }
            sectorNumber = await getNextSectorInChain(sectorNumber);
        }
        const sector = await readSector(sectorNumber);
        return [sector, desiredSectorCount];
    }
    async function readMiniStream(startMiniSectorNumber: number, streamSize: number): Promise<Uint8Array> {
        //根目录的数据流就是所有迷你扇区的存储容器
        //root.startSector 是迷你流数据的标准扇区起始位置
        const root = directoryTable[0];
        const result = new Uint8Array(streamSize);
        const startSectorNumber = root.startSector;
        let miniSectorNumber = startMiniSectorNumber;

        //定位并读取包含目标迷你扇区的标准扇区。
        let [sector, sectorCount] = await readSectorByMiniSectorNumber(startSectorNumber, miniSectorNumber);
        for (let miniSectorCount = 0; miniSectorCount < MAX_SECTORS_COUNT_IN_CHAIN; miniSectorCount++) {
            if (miniSectorNumber === END_OF_CHAIN) {
                break;
            }
            const desiredSectorCount = getSectorCountByMiniSectorNumber(miniSectorNumber);
            if (desiredSectorCount != sectorCount) {
                [sector, sectorCount] = await readSectorByMiniSectorNumber(startSectorNumber, miniSectorNumber);
            }
            // 计算相对位置:当前迷你扇区在标准扇区内的相对索引
            // 计算字节偏移:相对索引 × 迷你扇区大小(64字节)
            // 确定数据大小:处理最后一个迷你扇区可能不满的情况  
            // 提取并复制数据:从标准扇区中切片并复制到结果数组
            const miniSectorNumberFromStartOfSector =
                miniSectorNumber - sectorCount * countOfMiniSectorsInSector;
            const offset = miniSectorNumberFromStartOfSector * miniSectorSize;
            const dataSize = Math.min(
                miniSectorSize,
                streamSize - miniSectorCount * miniSectorSize
            );
            const array = new Uint8Array(sector.slice(offset, offset + dataSize));
            result.set(array, miniSectorCount * miniSectorSize);
            miniSectorNumber = getNextMiniSectorInChain(miniSectorNumber);
        }
        return result;
    }

本文介绍了从一个复合二进制文档的文档头开始,一步步解析各种扇区配置(主扇区配置,扇区配置,短扇区配置),再到解析文档的目录结构,最后是读取流与短流的二进制数据结束。

文中的方法,仅仅是读取文档结构的二进制数据,对文档内容还是知之甚少。

相关推荐
上单带刀不带妹17 分钟前
前端安全问题怎么解决
前端·安全
Fly-ping20 分钟前
【前端】JavaScript 的事件循环 (Event Loop)
开发语言·前端·javascript
SunTecTec43 分钟前
IDEA 类上方注释 签名
服务器·前端·intellij-idea
在逃的吗喽1 小时前
黑马头条项目详解
前端·javascript·ajax
袁煦丞1 小时前
有Nextcloud家庭共享不求人:cpolar内网穿透实验室第471个成功挑战
前端·程序员·远程工作
小磊哥er2 小时前
【前端工程化】前端项目开发过程中如何做好通知管理?
前端
拾光拾趣录2 小时前
一次“秒开”变成“转菊花”的线上事故
前端
你我约定有三2 小时前
前端笔记:同源策略、跨域问题
前端·笔记
JHCan3332 小时前
一个没有手动加分号引发的bug
前端·javascript·bug
pe7er2 小时前
懒人的代码片段
前端