From d0f3914ac1b1134387a077d3c76ee1d8eb756be1 Mon Sep 17 00:00:00 2001 From: tongjunhui Date: Tue, 5 Jul 2016 18:03:30 +0800 Subject: [PATCH] first worked version --- .gitattributes | 63 +++ .gitignore | 38 ++ LICENSE | 22 + README.md | 46 +- build.bat | 4 + makefile | 21 + samples/IEOpenedTabParser/IEOpenedTabParser.h | 59 +++ samples/IEOpenedTabParser/openedtab.cpp | 52 ++ samples/cfb/cfb.cpp | 217 ++++++++ src/include/compoundfilereader.h | 480 ++++++++++++++++++ src/include/utf.h | 137 +++++ test/data/1.dat | Bin 0 -> 8192 bytes test/data/2.dat | Bin 0 -> 5632 bytes test/data/a test email message.msg | Bin 0 -> 57344 bytes test/data/unicode.dat | Bin 0 -> 5632 bytes ...{B85C5677-E8BC-11E4-825B-10604B7CB9F0}.dat | Bin 0 -> 7680 bytes ...{BC59C035-E8AC-11E4-825B-10604B7CB9F0}.dat | Bin 0 -> 9728 bytes ...{FE554E21-EA21-11E4-825B-10604B7CB9F0}.dat | Bin 0 -> 9728 bytes .../IEOpenedTabParser.vcxproj | 95 ++++ .../IEOpenedTabParser.vcxproj.filters | 27 + vsproject/cfbreader/cfb/cfb.vcxproj | 92 ++++ vsproject/cfbreader/cfb/cfb.vcxproj.filters | 22 + vsproject/cfbreader/cfbreader.sln | 28 + 23 files changed, 1402 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 build.bat create mode 100644 makefile create mode 100644 samples/IEOpenedTabParser/IEOpenedTabParser.h create mode 100644 samples/IEOpenedTabParser/openedtab.cpp create mode 100644 samples/cfb/cfb.cpp create mode 100644 src/include/compoundfilereader.h create mode 100644 src/include/utf.h create mode 100644 test/data/1.dat create mode 100644 test/data/2.dat create mode 100644 test/data/a test email message.msg create mode 100644 test/data/unicode.dat create mode 100644 test/data/{B85C5677-E8BC-11E4-825B-10604B7CB9F0}.dat create mode 100644 test/data/{BC59C035-E8AC-11E4-825B-10604B7CB9F0}.dat create mode 100644 test/data/{FE554E21-EA21-11E4-825B-10604B7CB9F0}.dat create mode 100644 vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj create mode 100644 vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj.filters create mode 100644 vsproject/cfbreader/cfb/cfb.vcxproj create mode 100644 vsproject/cfbreader/cfb/cfb.vcxproj.filters create mode 100644 vsproject/cfbreader/cfbreader.sln diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad2f2b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +out/ + +# Visual Studio temp files +*.vcxproj.user +*.sdf +*.suo +*.opensdf +*.opendb +*.VC.db \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc27427 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md index 6997100..23b4c58 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ # compoundfilereader -A simple header file to read Microsoft compound file(Structured Storage File) with minimal efforts. +simple standalone c++ header file to read compound file (Structured Storage File) content. + +# Source code structure +- **src/include/compoundfilereader.h** + The only header file needed for parsing compound file. +- **src/include/utf.h** + The helper header file used for converting between utf16, utf8, and unicode. It's used by samples. +- **test/data** + Real world compound files for tests. +- **samples/cfb** + command line tool to list and dump compound files. +- **samples/IEOpenedTabParser** + command line tool to show IE opened tab information. +- **vsproject** + project and solution files for Microsoft Visual Studio. + +# Usage +- copy compoundfilereader.h to your source tree + or: install "compoundfilereader" by git sub-module +- #include in your source code +- construct a **CompoundFileReader** object by giving the buffer (see compoundfilereader.h for details) + +# Build the samples +## Linux +run `make' +(requires gcc and g++) +## Windows +option1: double click 'vsproject\cfbreader\cfbreader.sln' then build in Visual Studio +option2: run 'build.bat' in Visual Studio Command Prompt +(requires visual studio) +## Run the samples +try the following: +``` batchfile +out/ieot "test/data/{BC59C035-E8AC-11E4-825B-10604B7CB9F0}.dat" +out/cfb list "test/data/a test email message.msg" +out/cfb dump "test/data/a test email message.msg" __properties_version1.0 +``` + +# TODO +- unit tests +- make the reader able to connect to abstract interfaces such as istream + +#Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..9e2417a --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ +:: this script can only be run under visual studio tools command line +:: (search "Command Prompt" on start menu to pick one) + +devenv.com vsproject\cfbreader\cfbreader.sln /build Debug diff --git a/makefile b/makefile new file mode 100644 index 0000000..212021c --- /dev/null +++ b/makefile @@ -0,0 +1,21 @@ +IDIR=src/include +CFBHDR=$(wildcard $(IDIR)/*.h) +CC=g++ +CFLAGS=-I$(IDIR) -std=c++11 -Wall + +ODIR=out + +all: out/ieot out/cfb + +out/ieot: samples/IEOpenedTabParser/openedtab.cpp samples/IEOpenedTabParser/IEOpenedTabParser.h $(CFBHDR) + mkdir -p out + $(CC) -o $@ $< $(CFLAGS) + +out/cfb: samples/cfb/cfb.cpp $(CFBHDR) + mkdir -p out + $(CC) -o $@ $< $(CFLAGS) + +.PHONY: clean + +clean: + rm -rf out diff --git a/samples/IEOpenedTabParser/IEOpenedTabParser.h b/samples/IEOpenedTabParser/IEOpenedTabParser.h new file mode 100644 index 0000000..8a73a78 --- /dev/null +++ b/samples/IEOpenedTabParser/IEOpenedTabParser.h @@ -0,0 +1,59 @@ +/** + IE opened tab dat file parser + opened tab files are in %localappdata%\Microsoft\Internet Explorer\TabRoaming\{GUID}\NNNN, (N is digit 0-9). + They are compound file. +*/ + +#include +#include +#include +#include + +struct OPENED_TAB_INFO +{ + std::wstring url; + std::wstring title; + std::wstring faviconUrl; +}; + +struct OpenedTabFileParser +{ + static const CFB::COMPOUND_FILE_ENTRY* GetOpenedTabPropertyStream(CFB::CompoundFileReader& reader) + { + const CFB::COMPOUND_FILE_ENTRY* entry = nullptr; + reader.EnumFiles(reader.GetRootEntry(), -1, + [&](const CFB::COMPOUND_FILE_ENTRY* e, const CFB::utf16string&, int)->void + { + if (reader.IsPropertyStream(e)) + { + entry = e; + } + }); + + return entry; + } + + static bool GetOpenedTabInfo(const void* buffer, size_t bufferLen, OPENED_TAB_INFO* info) + { + CFB::CompoundFileReader reader(buffer, bufferLen); + const CFB::COMPOUND_FILE_ENTRY* propertyFile = GetOpenedTabPropertyStream(reader); + + bool ret = false; + if (propertyFile != nullptr && propertyFile->size <= std::numeric_limits::max()) + { + size_t size = static_cast(propertyFile->size); + + char* data = new char[size]; + reader.ReadFile(propertyFile, 0, data, size); + CFB::PropertySetStream propertySetStream(data, size); + CFB::PropertySet propertySet = propertySetStream.GetPropertySet(0); + info->url = UTF16ToWstring(propertySet.GetStringProperty(3)); + info->title = UTF16ToWstring(propertySet.GetStringProperty(5)); + info->faviconUrl = UTF16ToWstring(propertySet.GetStringProperty(1002)); + ret = true; + delete [] data; + } + + return ret; + } +}; diff --git a/samples/IEOpenedTabParser/openedtab.cpp b/samples/IEOpenedTabParser/openedtab.cpp new file mode 100644 index 0000000..4e03955 --- /dev/null +++ b/samples/IEOpenedTabParser/openedtab.cpp @@ -0,0 +1,52 @@ +#include "IEOpenedTabParser.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +int main(int argc, char* argv[]) +{ + if (argc != 2) + { + printf("please give a path\n"); + return 1; + } + + FILE* fp = fopen(argv[1], "rb"); + if (fp == NULL) + { + perror("read file error\n"); + return 1; + } + + fseek(fp, 0, SEEK_END); + size_t len = ftell(fp); + unsigned char* buffer = new unsigned char[len]; + fseek(fp, 0, SEEK_SET); + + len = fread(buffer, 1, len, fp); + printf("file length: %lu\n", len); + + OPENED_TAB_INFO info; + if (!OpenedTabFileParser::GetOpenedTabInfo(buffer, len, &info)) + { + printf("cannot find the property set\n"); + return 1; + } + + printf("url: %s\ntitle: %s\nfavicon url: %s\n", + WstringToUTF8(info.url.c_str()).c_str(), + WstringToUTF8(info.title.c_str()).c_str(), + WstringToUTF8(info.faviconUrl.c_str()).c_str()); + + return 0; +} diff --git a/samples/cfb/cfb.cpp b/samples/cfb/cfb.cpp new file mode 100644 index 0000000..c354713 --- /dev/null +++ b/samples/cfb/cfb.cpp @@ -0,0 +1,217 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +void ShowUsage() +{ + cout << + "usage:\n" + "cfb list FILENAME\n" + "cfb dump FILENAME STREAM_PATH\n" + "cfb info FILENAME\n" + "cfb info FILENAME STREAM_PATH\n" + << endl; +} + +void DumpBuffer(const void* buffer, size_t len) +{ + const unsigned char* str = static_cast(buffer); + for (size_t i = 0; i < len; i++) + { + if (i > 0 && i % 16 == 0) + cout << endl; + cout << setw(2) << setfill('0') << hex << static_cast(str[i]) << ' '; + } + cout << endl; +} + +void DumpText(const char* buffer, size_t len) +{ + cout << std::string(buffer, len) << endl; +} + +void OutputFileInfo(const CFB::CompoundFileReader& reader) +{ + const CFB::COMPOUND_FILE_HDR* hdr = reader.GetFileInfo(); + cout + << "file version: " << hdr->majorVersion << "." << hdr->minorVersion << endl + << "difat sector: " << hdr->numDIFATSector << endl + << "directory sector: " << hdr->numDirectorySector << endl + << "fat sector: " << hdr->numFATSector << endl + << "mini fat sector: " << hdr->numMiniFATSector << endl; +} + +void OutputEntryInfo(const CFB::CompoundFileReader& reader, const CFB::COMPOUND_FILE_ENTRY* entry) +{ + cout + << "entry type: " << (reader.IsPropertyStream(entry) ? "property" : (reader.IsStream(entry) ? "stream" : "directory")) << endl + << "color flag: " << entry->colorFlag << endl + << "creation time: " << entry->creationTime << endl + << "modified time: " << entry->modifiedTime << endl + << "child ID: " << entry->childID << endl + << "left sibling ID: " << entry->leftSiblingID << endl + << "right sibling ID: " << entry->startSectorLocation << entry->rightSiblingID << endl + << "start sector: " << entry->startSectorLocation << endl + << "size: " << entry->size << endl; +} + +const void ListDirectory(const CFB::CompoundFileReader& reader) +{ + reader.EnumFiles(reader.GetRootEntry(), -1, + [&](const CFB::COMPOUND_FILE_ENTRY* entry, const CFB::utf16string& dir, int level)->void + { + bool isDirectory = !reader.IsStream(entry); + std::string name = UTF16ToUTF8(entry->name); + std::string indentstr(level * 4 - 4, ' '); + cout << indentstr.c_str() << (isDirectory ? "[" : "") << name.c_str() << (isDirectory ? "]" : "") << endl; + }); +} + +const CFB::COMPOUND_FILE_ENTRY* FindStream(const CFB::CompoundFileReader& reader, const char* streamName) +{ + const CFB::COMPOUND_FILE_ENTRY* ret = nullptr; + reader.EnumFiles(reader.GetRootEntry(), -1, + [&](const CFB::COMPOUND_FILE_ENTRY* entry, const CFB::utf16string& u16dir, int level)->void + { + if (reader.IsStream(entry)) + { + std::string name = UTF16ToUTF8(entry->name); + if (u16dir.length() > 0) + { + std::string dir = UTF16ToUTF8(u16dir.c_str()); + if (strncmp(streamName, dir.c_str(), dir.length()) == 0 && + streamName[dir.length()] == '\\' && + strcmp(streamName + dir.length() + 1, name.c_str()) == 0) + { + ret = entry; + } + } + else + { + if (strcmp(streamName, name.c_str()) == 0) + { + ret = entry; + } + } + } + }); + return ret; +} + +int main_internal(int argc, char* argv[]) +{ + const char* cmd = nullptr; + const char* file = nullptr; + const char* streamName = nullptr; + bool dumpraw = false; + for (int i = 1; i < argc; i++) + { + if (i == 1) + { + cmd = argv[i]; + } + else if (strcmp(argv[i], "-r") == 0) + { + dumpraw = true; + } + else + { + if (file == nullptr) + file = argv[i]; + else + streamName = argv[i]; + } + } + + if (cmd == nullptr || file == nullptr) + { + ShowUsage(); + return 1; + } + + FILE* fp = fopen(file, "rb"); + if (fp == NULL) + { + cerr << "read file error" << endl; + return 1; + } + + fseek(fp, 0, SEEK_END); + size_t len = ftell(fp); + std::unique_ptr buffer(new unsigned char[len]); + fseek(fp, 0, SEEK_SET); + + len = fread(buffer.get(), 1, len, fp); + CFB::CompoundFileReader reader(buffer.get(), len); + + if (strcmp(cmd, "list") == 0) + { + ListDirectory(reader); + } + else if (strcmp(cmd, "dump") == 0 && streamName != nullptr) + { + const CFB::COMPOUND_FILE_ENTRY* entry = FindStream(reader, streamName); + if (entry == nullptr) + { + cerr << "error: stream doesn't exist" << endl; + return 2; + } + cout << "size: " << entry->size << endl; + if (entry->size > std::numeric_limits::max()) + { + cerr << "error: stream too large" << endl; + return 2; + } + size_t size = static_cast(entry->size); + std::unique_ptr content(new char[size]); + reader.ReadFile(entry, 0, content.get(), size); + if (dumpraw) + DumpText(content.get(), size); + else + DumpBuffer(content.get(), size); + } + else if (strcmp(cmd, "info") == 0) + { + if (streamName == nullptr) + { + OutputFileInfo(reader); + } + else + { + const CFB::COMPOUND_FILE_ENTRY* entry = FindStream(reader, streamName); + if (entry == NULL) + { + cerr << "error: stream doesn't exist" << endl; + return 2; + } + OutputEntryInfo(reader, entry); + } + } + else + { + ShowUsage(); + return 1; + } + + return 0; +} + +int main(int argc, char* argv[]) +{ + try + { + return main_internal(argc, argv); + } + catch (CFB::CFBException& e) + { + cerr << "error: " << e.what() << endl; + return 2; + } +} + diff --git a/src/include/compoundfilereader.h b/src/include/compoundfilereader.h new file mode 100644 index 0000000..b647a1f --- /dev/null +++ b/src/include/compoundfilereader.h @@ -0,0 +1,480 @@ +/** + Microsoft Compound File (and Property Set) Reader + http://en.wikipedia.org/wiki/Compound_File_Binary_Format + + Format specification: + MS-CFB: https://msdn.microsoft.com/en-us/library/dd942138.aspx + MS-OLEPS: https://msdn.microsoft.com/en-us/library/dd942421.aspx + + Note: + 1. For simplification, the code assumes that the target system is little-endian. + + 2. The reader operates the passed buffer in-place. + You must keep the input buffer valid when you are using the reader. + + 3. Single thread usage. + + Example 1: print all streams in a compound file + \code + CFB::CompoundFileReader reader(buffer, len); + reader.EnumFiles(reader.GetRootEntry(), -1, + [&](const CFB::COMPOUND_FILE_ENTRY* entry, const CFB::utf16string& dir, int level)->void + { + bool isDirectory = !reader.IsStream(entry); + std::string name = UTF16ToUTF8(entry->name); + std::string indentstr(level * 4, ' '); + printf("%s%s%s%s\n", indentstr.c_str(), isDirectory ? "[" : "", name.c_str(), isDirectory ? "]" : ""); + }); + \endcode +*/ + +#include +#include +#include +#include +#include +#include + +namespace CFB +{ + struct CFBException : public std::runtime_error + { + CFBException(const char* desc) : std::runtime_error(desc) {} + }; + struct WrongFormat : public CFBException + { + WrongFormat() : CFBException("Wrong file format") {} + }; + struct FileCorrupted : public CFBException + { + FileCorrupted() : CFBException("File corrupted") {} + }; + +#pragma pack(push) +#pragma pack(1) + +struct COMPOUND_FILE_HDR +{ + unsigned char signature[8]; + unsigned char unused_clsid[16]; + uint16_t minorVersion; + uint16_t majorVersion; + uint16_t byteOrder; + uint16_t sectorShift; + uint16_t miniSectorShift; + unsigned char reserved[6]; + uint32_t numDirectorySector; + uint32_t numFATSector; + uint32_t firstDirectorySectorLocation; + uint32_t transactionSignatureNumber; + uint32_t miniStreamCutoffSize; + uint32_t firstMiniFATSectorLocation; + uint32_t numMiniFATSector; + uint32_t firstDIFATSectorLocation; + uint32_t numDIFATSector; + uint32_t headerDIFAT[109]; +}; + +struct COMPOUND_FILE_ENTRY +{ + uint16_t name[32]; + uint16_t nameLen; + uint8_t type; + uint8_t colorFlag; + uint32_t leftSiblingID; // Note that it's actually the left/right child in the RB-tree. + uint32_t rightSiblingID; // So entry.leftSibling.rightSibling does NOT go back to entry. + uint32_t childID; + unsigned char clsid[16]; + uint32_t stateBits; + uint64_t creationTime; + uint64_t modifiedTime; + uint32_t startSectorLocation; + uint64_t size; +}; + +struct PROPERTY_SET_STREAM_HDR +{ + unsigned char byteOrder[2]; + uint16_t version; + uint32_t systemIdentifier; + unsigned char clsid[16]; + uint32_t numPropertySets; + struct + { + char fmtid[16]; + uint32_t offset; + } propertySetInfo[1]; +}; + +struct PROPERTY_SET_HDR +{ + uint32_t size; + uint32_t NumProperties; + struct + { + uint32_t id; + uint32_t offset; + } propertyIdentifierAndOffset[1]; +}; + +#pragma pack(pop) + +const size_t MAXREGSECT = 0xFFFFFFFA; + +struct helper +{ + static uint32_t ParseUint32(const void* buffer) + { + return *static_cast(buffer); + } +}; + +typedef std::basic_string utf16string; +typedef std::function + EnumFilesCallback; + +class CompoundFileReader +{ +public: + CompoundFileReader(const void* buffer, size_t len) + : m_buffer(static_cast(buffer)) + , m_bufferLen(len) + , m_hdr(static_cast(buffer)) + , m_sectorSize(512) + , m_minisectorSize(64) + , m_miniStreamStartSector(0) + { + if (buffer == NULL || len == 0) throw std::invalid_argument(""); + + if (m_bufferLen < sizeof(*m_hdr) || + memcmp(m_hdr->signature, "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1", 8) != 0) + { + throw WrongFormat(); + } + + m_sectorSize = m_hdr->majorVersion == 3 ? 512 : 4096; + + // The file must contains at least 3 sectors + if (m_bufferLen < m_sectorSize * 3) throw FileCorrupted(); + + const COMPOUND_FILE_ENTRY* root = GetEntry(0); + if (root == NULL) throw FileCorrupted(); + + m_miniStreamStartSector = root->startSectorLocation; + } + + /// Get entry (directory or file) by its ID. + /// Pass "0" to get the root directory entry. -- This is the start point to navigate the compound file. + /// Use the returned object to access child entries. + const COMPOUND_FILE_ENTRY* GetEntry(size_t entryID) const + { + if (entryID == 0xFFFFFFFF) + { + return NULL; + } + + if (m_bufferLen / sizeof(COMPOUND_FILE_ENTRY) <= entryID) + { + throw std::invalid_argument(""); + } + + size_t sector = 0; + size_t offset = 0; + LocateFinalSector(m_hdr->firstDirectorySectorLocation, entryID * sizeof(COMPOUND_FILE_ENTRY), §or, &offset); + return reinterpret_cast(SectorOffsetToAddress(sector, offset)); + } + + const COMPOUND_FILE_ENTRY* GetRootEntry() const + { + return GetEntry(0); + } + + const COMPOUND_FILE_HDR* GetFileInfo() const + { + return m_hdr; + } + + /// Get file(stream) data start with "offset". + /// The buffer must have enough space to store "len" bytes. Typically "len" is derived by the steam length. + void ReadFile(const COMPOUND_FILE_ENTRY* entry, size_t offset, char* buffer, size_t len) const + { + if (entry->size < offset || entry->size - offset < len) throw std::invalid_argument(""); + + if (entry->size < m_hdr->miniStreamCutoffSize) + { + ReadMiniStream(entry->startSectorLocation, offset, buffer, len); + } + else + { + ReadStream(entry->startSectorLocation, offset, buffer, len); + } + } + + bool IsPropertyStream(const COMPOUND_FILE_ENTRY* entry) const + { + // defined in [MS-OLEPS] 2.23 "Property Set Stream and Storage Names" + return entry->name[0] == 5; + } + + bool IsStream(const COMPOUND_FILE_ENTRY* entry) const + { + return entry->type == 2; + } + + void EnumFiles(const COMPOUND_FILE_ENTRY* entry, int maxLevel, EnumFilesCallback callback) const + { + utf16string dir; + EnumNodes(GetEntry(entry->childID), 0, maxLevel, dir, callback); + } + +private: + + // Enum entries with same level, including 'entry' itself + void EnumNodes(const COMPOUND_FILE_ENTRY* entry, int currentLevel, int maxLevel, + const utf16string& dir, EnumFilesCallback callback) const + { + if (maxLevel > 0 && currentLevel >= maxLevel) + return; + if (entry == nullptr) + return; + + callback(entry, dir, currentLevel + 1); + + const COMPOUND_FILE_ENTRY* child = GetEntry(entry->childID); + if (child != nullptr) + { + utf16string newDir = dir; + if (dir.length() != 0) + newDir.append(1, '\n'); + newDir.append(entry->name, entry->nameLen / 2); + EnumNodes(GetEntry(entry->childID), currentLevel + 1, maxLevel, newDir, callback); + } + + EnumNodes(GetEntry(entry->leftSiblingID), currentLevel, maxLevel, dir, callback); + EnumNodes(GetEntry(entry->rightSiblingID), currentLevel, maxLevel, dir, callback); + } + + void ReadStream(size_t sector, size_t offset, char* buffer, size_t len) const + { + LocateFinalSector(sector, offset, §or, &offset); + + // copy as many as possible in each step + // copylen typically iterate as: m_sectorSize - offset --> m_sectorSize --> m_sectorSize --> ... --> remaining + while (len > 0) + { + const unsigned char* src = SectorOffsetToAddress(sector, offset); + size_t copylen = std::min(len, m_sectorSize - offset); + if (m_buffer + m_bufferLen < src + copylen) throw FileCorrupted(); + + memcpy(buffer, src, copylen); + buffer += copylen; + len -= copylen; + sector = GetNextSector(sector); + offset = 0; + } + } + + // Same logic as "ReadStream" except that use MiniStream functions instead + void ReadMiniStream(size_t sector, size_t offset, char* buffer, size_t len) const + { + LocateFinalMiniSector(sector, offset, §or, &offset); + + // copy as many as possible in each step + // copylen typically iterate as: m_sectorSize - offset --> m_sectorSize --> m_sectorSize --> ... --> remaining + while (len > 0) + { + const unsigned char* src = MiniSectorOffsetToAddress(sector, offset); + size_t copylen = std::min(len, m_minisectorSize - offset); + if (m_buffer + m_bufferLen < src + copylen) throw FileCorrupted(); + + memcpy(buffer, src, copylen); + buffer += copylen; + len -= copylen; + sector = GetNextMiniSector(sector); + offset = 0; + } + } + + size_t GetNextSector(size_t sector) const + { + // lookup FAT + size_t entriesPerSector = m_sectorSize / 4; + size_t fatSectorNumber = sector / entriesPerSector; + size_t fatSectorLocation = GetFATSectorLocation(fatSectorNumber); + return helper::ParseUint32(SectorOffsetToAddress(fatSectorLocation, sector % entriesPerSector * 4)); + } + + size_t GetNextMiniSector(size_t miniSector) const + { + size_t sector, offset; + LocateFinalSector(m_hdr->firstMiniFATSectorLocation, miniSector * 4, §or, &offset); + return helper::ParseUint32(SectorOffsetToAddress(sector, offset)); + } + + // Get absolute address from sector and offset. + const unsigned char* SectorOffsetToAddress(size_t sector, size_t offset) const + { + if (sector >= MAXREGSECT || + offset >= m_sectorSize || + m_bufferLen <= static_cast(m_sectorSize) * sector + m_sectorSize + offset) + { + throw FileCorrupted(); + } + + return m_buffer + m_sectorSize + m_sectorSize * sector + offset; + } + + const unsigned char* MiniSectorOffsetToAddress(size_t sector, size_t offset) const + { + if (sector >= MAXREGSECT || + offset >= m_minisectorSize || + m_bufferLen <= static_cast(m_minisectorSize) * sector + offset) + { + throw FileCorrupted(); + } + + + LocateFinalSector(m_miniStreamStartSector, sector * m_minisectorSize + offset, §or, &offset); + return SectorOffsetToAddress(sector, offset); + } + + // Locate the final sector/offset when original offset expands multiple sectors + void LocateFinalSector(size_t sector, size_t offset, size_t* finalSector, size_t* finalOffset) const + { + while (offset >= m_sectorSize) + { + offset -= m_sectorSize; + sector = GetNextSector(sector); + } + *finalSector = sector; + *finalOffset = offset; + } + + void LocateFinalMiniSector(size_t sector, size_t offset, size_t* finalSector, size_t* finalOffset) const + { + while (offset >= m_minisectorSize) + { + offset -= m_minisectorSize; + sector = GetNextMiniSector(sector); + } + *finalSector = sector; + *finalOffset = offset; + } + + size_t GetFATSectorLocation(size_t fatSectorNumber) const + { + if (fatSectorNumber < 109) + { + return m_hdr->headerDIFAT[fatSectorNumber]; + } + else + { + fatSectorNumber -= 109; + size_t entriesPerSector = m_sectorSize / 4 - 1; + size_t difatSectorLocation = m_hdr->firstDIFATSectorLocation; + while (fatSectorNumber >= entriesPerSector) + { + fatSectorNumber -= entriesPerSector; + const unsigned char* addr = SectorOffsetToAddress(difatSectorLocation, m_sectorSize - 4); + difatSectorLocation = helper::ParseUint32(addr); + } + return helper::ParseUint32(SectorOffsetToAddress(difatSectorLocation, fatSectorNumber * 4)); + } + } + +private: + const unsigned char * m_buffer; + size_t m_bufferLen; + + const COMPOUND_FILE_HDR* m_hdr; + size_t m_sectorSize; + size_t m_minisectorSize; + size_t m_miniStreamStartSector; +}; + +class PropertySet +{ +public: + PropertySet(const void* buffer, size_t len, const char* fmtid) + : m_buffer(static_cast(buffer)) + , m_bufferLen(len) + , m_hdr(reinterpret_cast(buffer)) + , m_fmtid(fmtid) + { + if (m_bufferLen < sizeof(*m_hdr) || + m_bufferLen < sizeof(*m_hdr) + (m_hdr->NumProperties - 1) * sizeof(m_hdr->propertyIdentifierAndOffset[0])) + { + throw FileCorrupted(); + } + } + + /// return the string property in UTF-16 format + const uint16_t* GetStringProperty(uint32_t propertyID) + { + for (uint32_t i = 0; i < m_hdr->NumProperties; i++) + { + if (m_hdr->propertyIdentifierAndOffset[i].id == propertyID) + { + uint32_t offset = m_hdr->propertyIdentifierAndOffset[i].offset; + if (m_bufferLen < offset + 8) throw FileCorrupted(); + uint32_t stringLengthInChar = helper::ParseUint32(m_buffer + offset + 4); + if (m_bufferLen < offset + 8 + stringLengthInChar*2) throw FileCorrupted(); + return reinterpret_cast(m_buffer + offset + 8); + } + } + + return NULL; + } + + /// Note: Getting property of types other than "string" is not implemented yet. + /// However most other types are simpler than string so can be easily added. see [MS-OLEPS] + + const char* GetFmtID() + { + return m_fmtid; + } + +private: + const unsigned char* m_buffer; + size_t m_bufferLen; + const PROPERTY_SET_HDR* m_hdr; + const char* m_fmtid; // 16 bytes +}; + +class PropertySetStream +{ +public: + PropertySetStream(const void* buffer, size_t len) + : m_buffer(static_cast(buffer)) + , m_bufferLen(len) + , m_hdr(reinterpret_cast(buffer)) + { + if (m_bufferLen < sizeof(*m_hdr) || + m_bufferLen < sizeof(*m_hdr) + (m_hdr->numPropertySets - 1) * sizeof(m_hdr->propertySetInfo[0])) + { + throw FileCorrupted(); + } + } + + size_t GetPropertySetCount() + { + return m_hdr->numPropertySets; + } + + PropertySet GetPropertySet(size_t index) + { + if (index >= GetPropertySetCount()) throw FileCorrupted(); + uint32_t offset = m_hdr->propertySetInfo[index].offset; + if (m_bufferLen < offset + 4) throw FileCorrupted(); + uint32_t size = helper::ParseUint32(m_buffer + offset); + if (m_bufferLen < offset + size) throw FileCorrupted(); + return PropertySet(m_buffer + offset, size, m_hdr->propertySetInfo[index].fmtid); + } + +private: + const unsigned char * m_buffer; + size_t m_bufferLen; + const PROPERTY_SET_STREAM_HDR* m_hdr; +}; + +} diff --git a/src/include/utf.h b/src/include/utf.h new file mode 100644 index 0000000..5b253a8 --- /dev/null +++ b/src/include/utf.h @@ -0,0 +1,137 @@ +#pragma once + +#include +#include + +template +static bool GetNextCodePointFromUTF16z(const T* u16, size_t* pos, uint32_t* cp) +{ + *cp = static_cast(u16[*pos]); + if (*cp == 0) + return false; + + (*pos)++; + if ((*cp & 0xFC00) == 0xD800) + { + uint16_t cp2 = static_cast(u16[*pos]); + if ((cp2 & 0xFC00) == 0xDC00) + { + (*pos)++; + *cp = (*cp << 10) + cp2 - 0x35FDC00; + } + } + return true; +} + +template +static bool GetNextCodePointFromUTF16(const T* u16, size_t len, size_t* pos, uint32_t* cp) +{ + if (len == 0) + return GetNextCodePointFromUTF16z(u16, pos, cp); + + if (*pos >= len) + return false; + + *cp = static_cast(u16[*pos]); + (*pos)++; + if ((*cp & 0xFC00) == 0xD800) + { + if (*pos < len) + { + uint16_t cp2 = static_cast(u16[*pos]); + if ((cp2 & 0xFC00) == 0xDC00) + { + (*pos)++; + *cp = (*cp << 10) + cp2 - 0x35FDC00; + } + } + } + return true; +} + +static int CodePointToUTF8(uint32_t cp, uint32_t* c1, uint32_t* c2, uint32_t* c3, uint32_t* c4) +{ + if (cp < 0x80) + { + *c1 = cp; + return 1; + } + else if (cp <= 0x7FF) + { + *c1 = (cp >> 6) + 0xC0; + *c2 = (cp & 0x3F) + 0x80; + return 2; + } + else if (cp <= 0xFFFF) + { + *c1 = (cp >> 12) + 0xE0; + *c2 = ((cp >> 6) & 0x3F) + 0x80; + *c3 = (cp & 0x3F) + 0x80; + return 3; + } + else if (cp <= 0x10FFFF) + { + *c1 = (cp >> 18) + 0xF0; + *c2 = ((cp >> 12) & 0x3F) + 0x80; + *c3 = ((cp >> 6) & 0x3F) + 0x80; + *c4 = (cp & 0x3F) + 0x80; + return 4; + } + return 0; +} + +template +std::string UTF16ToUTF8(const T* u16, size_t len = 0) +{ + std::string u8; + uint32_t cp; + size_t pos = 0; + while (GetNextCodePointFromUTF16(u16, len, &pos, &cp)) + { + uint32_t c[4]; + int count = CodePointToUTF8(cp, c, c+1, c+2, c+3); + for (int i = 0; i < count; i++) + { + u8 += static_cast(c[i]); + } + } + return u8; +} + +template +std::wstring UTF16ToWstring(const T* u16, size_t len = 0) +{ + std::wstring ret; +#ifdef _MSC_VER + while (*u16) ret += *u16++; +#else + uint32_t cp; + size_t pos = 0; + while (GetNextCodePointFromUTF16(u16, len, &pos, &cp)) + { + ret += cp; + } +#endif + return ret; +} + +template +std::string WstringToUTF8(const T* wstr) +{ +#ifdef _MSC_VER + return UTF16ToUTF8(wstr); +#else + std::string u8; + uint32_t cp; + while ((cp = *wstr++) != 0) + { + uint32_t c[4]; + int count = CodePointToUTF8(cp, c, c+1, c+2, c+3); + for (int i = 0; i < count; i++) + { + u8 += static_cast(c[i]); + } + } + return u8; +#endif +} diff --git a/test/data/1.dat b/test/data/1.dat new file mode 100644 index 0000000000000000000000000000000000000000..d779d6ed1a68bca057f94a65879181e4f9807d22 GIT binary patch literal 8192 zcmeI1OH&kA6vt2V64V$UNDR@~iBV%w$`DbI63ZasBC*QES!Ab!4loLc3=b)_Y8I)A zo2*zGmFB8zN<{Qlk3OasC+15-GuIa8<4y{FH;=RW`U+@9Iz zyI%bIr17OKp>tMgbMtjp6Vf}RD}ww^vn`~ho134Xm%0)rS*8*if$!}OzcF*|q75TQ zZAw!anHsEmVa~M{R-@U+Pp2y%%sSN=ov-yRtL+DTj^O*Q_1kT`X)#h$v}qjfw}i#* zmYsq}ENQL0zlh9HyHz+5ky;(igt;Gt(@t&{=>KwFhJ&(RV>a(655%`vjQf>!E z+p~soyvCvwTEV8a0RJCS*N*H)Ry*E?J3#sablTIRt)wNlWp&bP>ZB`E>5sr>?f;Ax z7W6;Z{~n$NE5Yy8>dy_{|4!gra&^!C+wlGm$_hh;1KQu(|I#b1X_8c%r4woY*M;)F zw)`x`5zzV5!dwNrvVqtvqxqnhmbBmZATDYSx3A?M#`)D@h6D^$BCb$H&_ zRB(fg*W-VV8LO#Wi0T(wq*$))S{YUbq4N2$s#q$VT`R zct3mq{uI_&xUg)>w0R?-eZ1N8|2b_q1b+c*?;U})x4(ps!R0kly!EjZ#d7$POT7ze zm=^mJcI>Zb6$RkV1(5jbEDl}vaOyV5H0FJU(5=6p+z_m95(X!^UR`iDiJu`-J?O^i)lKqhkIo7gBvvo<6=KxejkwQ8Za#x(@0a^`q7_`h ze;NOaw^xf%^A)F_vp92g|4+YkrFQ=Irq-Hbv6&7hkuY>V-} zl>NWIahQKc>}0~iXu2Frb8+29BdXSl?zN)x@byb0-{}9VKqsknzg|+rRcJl&cb?zp TBA?&uZToutm+v2&d;j%Or?>-5c&H=3R=gsN39X8|hhHe;q3`=JfmeM?vbZkjnwf`b;bsNJ(xFl7VQLVY)M z1KROOzqvPRe$Rc7;{s7ekJ%&o9IpU&>8-+=Iv2?dqtWw}yWj28G;=YM|p&VR_iFIyawP2wUt|L*!La_4l@ zLh}gx=ijrP+A9ut=U#I0HOT!k$xBc?7SR3EoX1Ckigt4U?5FejpaTTZ^Y2Cg^<7sf zsX&p+p#ct=XL7iWMN5qO4?oO9lJt4md(kMZr(SBEJ|(C z9Z$Jd0C4i@gz@%>od;X2)7q?y@`Cm7{>)Wxp!E7b0{uHY{U^`pF9*Rk#Z<$L<(Q_T0`|6bnruBcwwYv=cU N!2NT!x3j^te*n1CrCb02 literal 0 HcmV?d00001 diff --git a/test/data/a test email message.msg b/test/data/a test email message.msg new file mode 100644 index 0000000000000000000000000000000000000000..26c99e0c378dea2ad51a1a744e8a02b7f7993b85 GIT binary patch literal 57344 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 literal 0 HcmV?d00001 diff --git a/test/data/unicode.dat b/test/data/unicode.dat new file mode 100644 index 0000000000000000000000000000000000000000..179e1e424f59880d00fb9fc30311243d13818694 GIT binary patch literal 5632 zcmeHK&rcIk5dKQ}Q3Op@LSkdmUW_IN+yX-JV6?P)X)uucD($G}(o z0$1V1cD51qF1d$%jq~mY8punwoy}$?_7cffp^PkW8`FGd5JDVNq>nJKqbwR)QMY^M zyS`CiwtuqslaX2V_}Kq>XK)<`sWVO84>5-O7==M@p5OF@I0h3*+{0yJ8Yy(KKTf&_ z5qhNC+Ndt6{SxwZ0Pe zFWoCU+7!E6ZUd5+Oem=1IGd}sm1u!~75^Cp4dD;OzsG$p_Ns@s6%_y0{%g5q+2Qb= z+quVyf5uRP$yz2xE>DR;{xuR`TmK=)6$7^f*Prj+}qiEAwtSg#bZ`!q_Kf7ChU ztUrbp1d$my`)VmV;eWU8!$rebY=@bVRmNEn&am>jf=hfR@tAemIdrgMF)@r0%4~>` zBsqG5BSIW+vT{q28zDD^Ga=QN!4(?6ebzLWp8kLn%Zj`GUkf9L$0=?BNFzEC-bZ^by{-JAR8 zF2Cqmsec^^4BuSzbZ%`f1Yhbub(|gY1NEnggv?Vm-*Zuz!Zr`1% literal 0 HcmV?d00001 diff --git a/test/data/{B85C5677-E8BC-11E4-825B-10604B7CB9F0}.dat b/test/data/{B85C5677-E8BC-11E4-825B-10604B7CB9F0}.dat new file mode 100644 index 0000000000000000000000000000000000000000..5038bfd6bc5f81b2999b2b8ec7ba1754d9d9a9a1 GIT binary patch literal 7680 zcmeI1PfXNT5XWcbkEpGpQmYhodoXGY-9iJJD(Q@!@kLr?9g96flz#6u+>xdQ!s-|mxNHt4R4`~b7_%k0c=-n{=a@6G&n z{@;TO4<9!_vm*GyYHVe7kJSg{5%^vHz0s@zUb2}anS#LUClw~fc(GM@BO>RE&tyG`g&Em zvsYjRi>a@971gn`dC>LH^=JLh&CP>-bCLGtrF$pu>YjRus%%8%6CjZLo}d4?Pjk>0 z(0kBD=mJzNcc856_H(H0ERIk%^fy#@$~;sy`4pH+cA%V09u8pUcsG$|fc{Gwv7DqFVG>LW(zuk6;_dB$fZ}${*j96%z z8D6v)&n=tcZNvsGL!>f_G(inv%*m&qBQB1}*bq^~xP6AEIDy8j`D)BF;M=#IEc6>A zT1hjiEbV()50Og-siL=-eZ%N7j`QXHXlvZp(MeK2Yu_=OG+c}kD*k9=OqGJ-5ACh? zM4nJiJ54A}%YXp&e?HGv^jk^%(@alK3jJ5qX;G!%2~boN%Iik@*D`4Dw1@IN`KLm2 z#f7?}Ww$==^LK;pg9FG(0qr1o2$YRW{t@&D_%V1CJO=vvBRI|~zdcsq9r%A_BmR$h zY#4$ox#7ktXkaJzAC~`r!n(GA$H5bz?!qYeDOf!pf)*4mKh77G6!AiaxL}+ex#A)N zeGi#*F&^7f3Zm;NRL}o#_p#F6vc!T}Vpn|u9AMW|b_4DbvKj1Qb8KThd#TE{W(AZp zl+G0LW4jG0tsAv7*iE|dO*Kolau$nf1^YQ=hFk^`zRkVPKmM>L?xNh4mLDG^GmVcH z8|+*>KKF~Ia+>pY%G9!W<_(wIO=)O=^6ql>LwV?0-QvCQB{6F!Pt-T|6!~$R^ZhPz z<{_R=WW%mBMac-yFu8L(`EM7cY2LcY;*=i`!gsh#JpWZ&_k17NkJco)X&O5igiCR{ z46(jZviJmC!qw`emD9+SWluBeD6Iu8sd(&_*%8BW`HJY5VEotRJNjQjIgCJ5IZ?R~ xLGf+$gYEvduKnG&u_?JTG*9q5!YEp5R9nsi|z z3r$!O6A~pZjnRaSF(Izm$ihDW6JwB=xM2(R^Sv{_cG}L2znLjW&D>w^@7#OtdEDnY z=iY0-th@g84>iBpy!3`u*`4V%wlWJJpuEhhuQhv!vT%2%r>BLkB2%oSB{T#3ZG>;a zg4S*+;Fz7(QunwZ^(!^ue@s3^aiK=-M@}t7H#3CN-|UAe4~_I^&vqWN zIq0p_Zp^`yS1DG1dD<3om!tmICQ1^jPQcEE^aEw|Q zd(?w{KS+5GquS3n_OhH|_1v?XjKUC!yOxKZM^& zKI7V3fs%XB;Z>GA$q`?dy-$5NG9_R99kfY)2eNzM-RJ1Kp$R$q4(1|GyZRA#`>)LS zFCbqbAL4iaXUBgfZTfjk((9#pP4}eg=@z^v3g;;)M}NiscXDn^PxpcSFZ%s2+x~0h zaz*oFK%Hqn>1tIq@$uAw4WE70mRx)3=4z`Ag)UyM>YEbx{D^rN%X$>so5~p}D;^$p ztqIz_s9AnPRz1eoPf4+S6R?3&lz*SYwYXA0xG?^p8E(BKEB+sVD+*>Sy8E#6@?&9Y zUU>w+mBdaa>`l0&KkemuDGlHO#Gih7Zb7f^5)tPGTAY6Ah*63GBT&{MSDr!sFY0b%Q1e@Ha9!staZKYg?X; zhvqI#mxd`vRSQlxlOxoIp%rDWwO=anzwV=oT@^p-j!{X)&w%2UA?8m>_qT4h509}! zQk>L!_hmv=M^oSzEYOy>sUNKi?MJ8OwjagbxYKI2Pv%Cnh3?i(#HAs3F7kGIwjnrM zEUh$=bNq)EY5WN+$|z@j1iO*o)617qaWf(-8gd>Y&0X*x%+&33LGvt2`h{S?yx&=w`!s7e?(q1f!Jrn*%&#;3FhF(G3wHYY9{)^*3 X%3uA;`8PaLuJeB?r~UiTxBdGEt`LDn literal 0 HcmV?d00001 diff --git a/test/data/{FE554E21-EA21-11E4-825B-10604B7CB9F0}.dat b/test/data/{FE554E21-EA21-11E4-825B-10604B7CB9F0}.dat new file mode 100644 index 0000000000000000000000000000000000000000..1cf1a6765dd93241f703e36afb590eade7aea66b GIT binary patch literal 9728 zcmeI1&u>&!6vxj1#Uh}fh*(huR75K?Q)np=LE2I)rc|V*Vl?7Fr^5*C4DHZD3}hB0 z5)%_PMi-J47skYm8rQ}|<5vCv7nneztX!~gL+j`Je$bJ;mN(NunK*ZH-?{g`bMLw5 zo^#KSHq^i9xpSB5<7Y2N?&IYTi(QO5I|$7jv`<*tj@f`E zXq{$HC#h*mTA#gVE!3Q)teN+n!0EEp=0)o1`bR$fX7*M>%1Y^jdeX*N^SE_86y5Q# z&lTJVpSEC_c}qch+C%JvY9h>}8F1?1zf}4Uu-4DIxRgcIn?(!gE~^MW$UC(qS3&Jt z|{>}Se_-gw87o<#-3fzCkC;1X^SH1JIg7-gCd=KtS+3a4r|Ngnj zhWd8Ew*k8UbyVSKO#4<__kTH6d~2Lgpk@bjFjZLmm`?6+0afyry_b)cPRKtf&XcTV z^CfRp>4xMkJ$Q)vFm(m>5vtxl3S32f%-c|HblW+MG+N@-HSh$&Fn&|5jFi>y^}__4|b%H8dRVx)|Af^Tzo#=bStR zMW4;|1U!!1KWCz$-wldfHC2X9vozyV+OyMfOj!m0ZK>|4MLgcqB!Clh-Z>lwaVi}*3uC+=j0?fAepzSs`1Mhc(Vh?hO((Hr3VZVDQC){3&4JnJxC zQ6onk@0Vm=6knMI>g#Nj)*#Pbdh*Tkw`q2$net^vcxLIjymUah&*7rO*^zKXuMJ;LG-&VnEp7F zW6Vjiq9iyS?F7d$?m_Gd;H1=*?v`}A1Nq-lGXE~8`Vw+>?2C7-XK~Mxsdry%di&aV z?}fXycbDeeo}=6b#qTQ0i(WyyhR_oC7VSmS!Mao_o~r%)hXeKJV9EL;8>(DF_A^d< z#IaE$+%VaIA?~YN^o{N0r-wgT+H>%&4R0KBv~Bgkx-*rMheswf_%@LL&u-)2&!Tke zWkJ%8%}v8q%CjLc=O4Z}nNJ!EmBMq3U}1J}9wWA%euj2}b7;oX_tPK5V|Cab#@iWd zBZ|%u3+%xMy-DwN=A<1f>8~Tt=AWVR*RG-+%;oH=U4Mr-mpI-##ebl?In6P8`_M!` zKlR%^oL8P`K>y1QBbm-34OIjnE8=FYCmgRd3ibhTjw3hUi}_kmdM@4PPdT>AF6zFiYd^;L zui76IWTkejRft# MIr2~YF|+0U8xI9gI{*Lx literal 0 HcmV?d00001 diff --git a/vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj b/vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj new file mode 100644 index 0000000..6f58194 --- /dev/null +++ b/vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj @@ -0,0 +1,95 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {7CD5AD07-5FE9-465D-A290-CAB53C97D545} + Win32Proj + IEOpenedTabParser + + + + Application + true + v140 + Unicode + + + Application + false + v140 + true + Unicode + + + + + + + + + + + + + true + $(SolutionDir)..\..\out\ + $(SolutionDir)..\..\out\intermediate\$(ProjectName)\ + + + false + $(SolutionDir)..\..\out\ + $(SolutionDir)..\..\out\intermediate\$(ProjectName)\ + + + + + + Level3 + Disabled + WIN32;_CRT_SECURE_NO_WARNINGS;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + $(SolutionDir)/../../src/include + + + Console + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;_CRT_SECURE_NO_WARNINGS;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + $(SolutionDir)/../../src/include + + + Console + true + true + true + + + + + + + + + + + + \ No newline at end of file diff --git a/vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj.filters b/vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj.filters new file mode 100644 index 0000000..7a22140 --- /dev/null +++ b/vsproject/cfbreader/IEOpenedTabParser/IEOpenedTabParser.vcxproj.filters @@ -0,0 +1,27 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + + + Source Files + + + \ No newline at end of file diff --git a/vsproject/cfbreader/cfb/cfb.vcxproj b/vsproject/cfbreader/cfb/cfb.vcxproj new file mode 100644 index 0000000..61545e5 --- /dev/null +++ b/vsproject/cfbreader/cfb/cfb.vcxproj @@ -0,0 +1,92 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {CED76631-0BBD-4775-9284-E3CF0E5A7A92} + Win32Proj + cfb + + + + Application + true + v140 + Unicode + + + Application + false + v140 + true + Unicode + + + + + + + + + + + + + true + $(SolutionDir)..\..\out\ + $(SolutionDir)..\..\out\intermediate\$(ProjectName)\ + + + false + $(SolutionDir)..\..\out\ + $(SolutionDir)..\..\out\intermediate\$(ProjectName)\ + + + + + + Level3 + Disabled + WIN32;_CRT_SECURE_NO_WARNINGS;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + $(SolutionDir)/../../src/include + + + Console + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;_CRT_SECURE_NO_WARNINGS;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + $(SolutionDir)/../../src/include + + + Console + true + true + true + + + + + + + + + \ No newline at end of file diff --git a/vsproject/cfbreader/cfb/cfb.vcxproj.filters b/vsproject/cfbreader/cfb/cfb.vcxproj.filters new file mode 100644 index 0000000..bb9204a --- /dev/null +++ b/vsproject/cfbreader/cfb/cfb.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file diff --git a/vsproject/cfbreader/cfbreader.sln b/vsproject/cfbreader/cfbreader.sln new file mode 100644 index 0000000..41e3969 --- /dev/null +++ b/vsproject/cfbreader/cfbreader.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IEOpenedTabParser", "IEOpenedTabParser\IEOpenedTabParser.vcxproj", "{7CD5AD07-5FE9-465D-A290-CAB53C97D545}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cfb", "cfb\cfb.vcxproj", "{CED76631-0BBD-4775-9284-E3CF0E5A7A92}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7CD5AD07-5FE9-465D-A290-CAB53C97D545}.Debug|Win32.ActiveCfg = Debug|Win32 + {7CD5AD07-5FE9-465D-A290-CAB53C97D545}.Debug|Win32.Build.0 = Debug|Win32 + {7CD5AD07-5FE9-465D-A290-CAB53C97D545}.Release|Win32.ActiveCfg = Release|Win32 + {7CD5AD07-5FE9-465D-A290-CAB53C97D545}.Release|Win32.Build.0 = Release|Win32 + {CED76631-0BBD-4775-9284-E3CF0E5A7A92}.Debug|Win32.ActiveCfg = Debug|Win32 + {CED76631-0BBD-4775-9284-E3CF0E5A7A92}.Debug|Win32.Build.0 = Debug|Win32 + {CED76631-0BBD-4775-9284-E3CF0E5A7A92}.Release|Win32.ActiveCfg = Release|Win32 + {CED76631-0BBD-4775-9284-E3CF0E5A7A92}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal