Microsoft Office ファイルの判別方法

ファイルタイプ判別関数群 - Life like a clown の補足記事.ここでは,Office 関連のファイルの判別方法だけ記述してみます.

OLE2

旧 Office ファイル (DOC, XLS, PPT) は OLE2 と呼ばれるフォーマットで記述されています.したがって,DOC, XLS, PPT ファイルの判別を行う場合には,まず OLE2 ファイルかどうかの判別を行います.

inline bool is_ole2(std::basic_istream<char>& in) {
    static const unsigned char sig[8] = { 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1 };
    
    unsigned char magic[8];
    in.read(reinterpret_cast<char*>(magic), 8);
    if (in.fail() || in.gcount() < 8) return false;
    if (std::memcmp(magic, sig, 8) != 0) return false;
    
    return true;
}

OLE2 ファイルは,ファイルの先頭に 512Byte のヘッダが付与されます.このヘッダの先頭 16Byte が "D0 CF 11 E0 A1 B1 1A E1" であるかどうかで OLE2 ファイルであるかどうかを判別します.

DOC (Microsoft Word)

DOC ファイルは,OLE2 ヘッダの直後に FIB (File Information Block) と呼ばれるヘッダが付与されます.この最初の 2 バイトが Magic Number (wIdent = 0xA5EC)なのでこの値で DOC ファイルかどうかを判別します(参考: FibBase - MSDN).

inline bool is_doc(std::basic_istream<char>& in) {
    static const unsigned short wIdent = 0xa5ec;
    
    if (!is_ole2(in)) return false;
    
    in.seekg(512); // OLE2 ヘッダサイズ分読み飛ばす
    unsigned short magic;
    in.read(reinterpret_cast<char*>(&magic), 2);
    if (in.fail() || in.gcount() < 2) return false;
    if (magic != wIdent) return false;
    
    return true;
}

XLS (Microsoft Excel)

XLS ファイルと PPT ファイルは判別がなかなか困難です.本当は OLE2 ライブラリなどを使用して判別するのが良いのですが,ここでは自力で判別してみます.

まず,OLE2 ヘッダにデータの開始地点が書かれているので(48--51Byte),その地点まで seek します.

ここでのデータは 128Byte を一つのブロックとして格納されています.また,最初の 128Byte は OLE2 共通(?)の "Root Entry" と呼ばれるブロックなので,このブロックは読み飛ばします.Excel の場合は,この直後に "Workbook" と呼ばれるブロックが出現するので,そのブロックの先頭の Magic Number を用いて判別します(古いバージョンの Excel の場合,"Book" かも?).

inline bool is_xls(std::basic_istream<char>& in) {
    static const unsigned short sig[] = { // "Workbook"
        0x57, 0x6f, 0x72, 0x6b, 0x62, 0x6f, 0x6f, 0x6b };
    static const unsigned short old[] = { // "Book"
        0x42, 0x6f, 0x6f, 0x6b };
    
    if (!is_ole2(in)) return false;
    
    /*
     * OLE2 は,セクターと呼ばれる単位でデータを管理している.
     * 30 -- 31Byte に 1 セクター当たりのバイト数が格納されている.
     * 格納されている値は, 2^(bits) と言う形で使用する.
     * ほとんどの場合,ここの値は 9(すなわち,1 セクターは 2^9 = 512Byte).
     */
    in.seekg(30);
    unsigned short bits = 0;
    in.read(reinterpret_cast<char*>(&bits), 2);
    if (in.fail() || in.gcount() < 2) return false;
		
    in.seekg(48);
    size_t pos = 0;
    in.read(reinterpret_cast<char*>(&pos), 4);
    if (in.fail() || in.gcount() < 4) return false;
    in.seekg(512 + pos * (1 << bits) + 128);
    
    unsigned short magic[64];
    in.read(reinterpret_cast<char*>(magic), 128);
    if (in.fail() || in.gcount() < 128) return false;
    if (std::memcmp(magic, sig, sizeof(sig)) != 0 &&
        std::memcmp(magic, old, sizeof(old)) != 0) {
        return false;
    }
    
    return true;
}

PPT (Microsoft PowerPoint)

PPT はもうすこし複雑になります.PPT も XLS と同様の方法を用いて,"PowerPoint Document" と呼ばれるブロックを見つければ良いのですが,ユーザが高速保存を行っているかどうか,画像ファイルを含んでいるかどうか,で出現するブロックの順番が異なるようです.

ユーザが高速保存を行っている場合は,"Current User" と呼ばれるブロックが "PowerPoint Document" ブロックよりも先に出現します.また,画像ファイルを含んでいる場合は,"Pictures" と呼ばれるブロックが出現することもあるようです.

以上から,"Pictures" ブロックは読み飛ばして,"Current User" ブロックか "PowerPoint Document" ブロックが存在するかどうかで判別する事とします.

inline bool is_ppt(std::basic_istream<char>& in) {
    static const unsigned short usr[] = { // "Current User"
        0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x20,
        0x55, 0x73, 0x65, 0x72 };
    static const unsigned short sig[] = { // "PowerPoint Document"
        0x50, 0x6f, 0x77, 0x65, 0x72, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x20,
        0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74 };
    static const unsigned short pic[] = { // "Pictures"
        0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x73 };
    
    if (!is_ole2(in)) return false;
    
    in.seekg(30);
    unsigned short bits = 0;
    in.read(reinterpret_cast<char*>(&bits), 2);
    if (in.fail() || in.gcount() < 2) return false;
		
    in.seekg(48);
    size_t pos = 0;
    in.read(reinterpret_cast<char*>(&pos), 4);
    if (in.fail() || in.gcount() < 4) return false;
    in.seekg(512 + pos * (1 << bits) + 128);
    
    unsigned short magic[64];
    in.read(reinterpret_cast<char*>(magic), 128);
    if (in.fail() || in.gcount() < 128) return false;
    if (std::memcmp(magic, pic, sizeof(pic)) == 0) {
        in.read(reinterpret_cast<char*>(magic), 128);
        if (in.fail() || in.gcount() < 128) return false;
    }
    
    if (std::memcmp(magic, sig, sizeof(sig)) != 0 &&
        std::memcmp(magic, usr, sizeof(usr)) != 0) {
        return false;
    }
    
    return true;
}

DOCX (Microsoft Word 2007)

Microsoft Office 2007 で新たに導入されたファイル形式 (DOCX, XLSX, PPTX) は,中身は複数のファイルを zip で圧縮しただけのものです.なので,ここでは clx::unzip を用いて解凍し,特定の (OpenXML) ファイルが存在するかどうかで判別します.

DOCX では,起点となるファイルは doc/document.xml です.

inline bool is_docx(const std::basic_string<char>& path) {
    basic_unzip<char> in(path);
    if (!in.is_open()) return false;
    if (in.find("word/document.xml") == in.end()) return false;
    return true;
}

XLSX (Microsoft Excel 2007)

DOCX と同様の方法を用いて判別します.XLSX では,起点となるファイルは xl/workbook.xml です.

inline bool is_xlsx(const std::basic_string<char>& path) {
    basic_unzip<char> in(path);
    if (!in.is_open()) return false;
    if (in.find("xl/workbook.xml") == in.end()) return false;
    return true;
}

PPTX (Microsoft PowerPoint 2007)

DOCX, XLSX と同様.起点となるファイルは,ppt/presentation.xml です.

inline bool is_pptx(const std::basic_string<char>& path) {
    basic_unzip<char> in(path);
    if (!in.is_open()) return false;
    if (in.find("ppt/presentation.xml") == in.end()) return false;
    return true;
}

以上です.それぞれ,結構不安の残る判別方法ですが,上記で概ね判別できるのではと思います.