// MIT License // // Copyright(c) 2016 Matthias Moeller // // 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. #ifndef __TYTI_STEAM_VDF_PARSER_H__ #define __TYTI_STEAM_VDF_PARSER_H__ #include #include #include #include #include #include #include #include #include #include #include #include // for wstring support #include #include // internal #include // VS < 2015 has only partial C++11 support #if defined(_MSC_VER) && _MSC_VER < 1900 #ifndef CONSTEXPR #define CONSTEXPR #endif #ifndef NOEXCEPT #define NOEXCEPT #endif #else #ifndef CONSTEXPR #define CONSTEXPR constexpr #define TYTI_UNDEF_CONSTEXPR #endif #ifndef NOEXCEPT #define NOEXCEPT noexcept #define TYTI_UNDEF_NOEXCEPT #endif #endif namespace tyti { namespace vdf { namespace detail { /////////////////////////////////////////////////////////////////////////// // Helper functions selecting the right encoding (char/wchar_T) /////////////////////////////////////////////////////////////////////////// template struct literal_macro_help { static CONSTEXPR const char *result(const char *c, const wchar_t *) NOEXCEPT { return c; } static CONSTEXPR char result(const char c, const wchar_t) NOEXCEPT { return c; } }; template <> struct literal_macro_help { static CONSTEXPR const wchar_t *result(const char *, const wchar_t *wc) NOEXCEPT { return wc; } static CONSTEXPR wchar_t result(const char, const wchar_t wc) NOEXCEPT { return wc; } }; #define TYTI_L(type, text) \ vdf::detail::literal_macro_help::result(text, L##text) inline std::string string_converter(const std::string &w) NOEXCEPT { return w; } inline std::string string_converter(const std::wstring &w) NOEXCEPT { std::mbstate_t state = std::mbstate_t(); auto wstr = w.data(); // unsafe: ignores any error handling // and disables warning that wcsrtombs_s should be used #ifdef WIN32 #pragma warning(push) #pragma warning(disable : 4996) #endif std::size_t len = 1 + std::wcsrtombs(nullptr, &wstr, 0, &state); std::string mbstr(len, '\0'); std::wcsrtombs(&mbstr[0], &wstr, mbstr.size(), &state); #ifdef WIN32 #pragma warning(pop) #endif return mbstr; } /////////////////////////////////////////////////////////////////////////// // Writer helper functions /////////////////////////////////////////////////////////////////////////// template class tabs { const size_t t; public: explicit CONSTEXPR tabs(size_t i) NOEXCEPT : t(i) {} std::basic_string print() const { return std::basic_string(t, TYTI_L(charT, '\t')); } inline CONSTEXPR tabs operator+(size_t i) const NOEXCEPT { return tabs(t + i); } }; template oStreamT &operator<<(oStreamT &s, const tabs t) { s << t.print(); return s; } } // end namespace detail /////////////////////////////////////////////////////////////////////////// // Interface /////////////////////////////////////////////////////////////////////////// /// custom objects and their corresponding write functions /// basic object node. Every object has a name and can contains attributes saved /// as key_value pairs or childrens template struct basic_object { typedef CharT char_type; std::basic_string name; std::unordered_map, std::basic_string> attribs; std::unordered_map, std::shared_ptr>> childs; void add_attribute(std::basic_string key, std::basic_string value) { attribs.emplace(std::move(key), std::move(value)); } void add_child(std::unique_ptr> child) { std::shared_ptr> obj{child.release()}; childs.emplace(obj->name, obj); } void set_name(std::basic_string n) { name = std::move(n); } }; template struct basic_multikey_object { typedef CharT char_type; std::basic_string name; std::unordered_multimap, std::basic_string> attribs; std::unordered_multimap, std::shared_ptr>> childs; void add_attribute(std::basic_string key, std::basic_string value) { attribs.emplace(std::move(key), std::move(value)); } void add_child(std::unique_ptr> child) { std::shared_ptr> obj{child.release()}; childs.emplace(obj->name, obj); } void set_name(std::basic_string n) { name = std::move(n); } }; typedef basic_object object; typedef basic_object wobject; typedef basic_multikey_object multikey_object; typedef basic_multikey_object wmultikey_object; struct Options { bool strip_escape_symbols; bool ignore_all_platform_conditionals; bool ignore_includes; Options() : strip_escape_symbols(true), ignore_all_platform_conditionals(false), ignore_includes(false) { } }; // forward decls // forward decl template OutputT read(iStreamT &inStream, const Options &opt = Options{}); /** \brief writes given object tree in vdf format to given stream. Output is prettyfied, using tabs */ template void write(oStreamT &s, const T &r, const detail::tabs tab = detail::tabs(0)) { typedef typename oStreamT::char_type charT; using namespace detail; s << tab << TYTI_L(charT, '"') << r.name << TYTI_L(charT, "\"\n") << tab << TYTI_L(charT, "{\n"); for (const auto &i : r.attribs) s << tab + 1 << TYTI_L(charT, '"') << i.first << TYTI_L(charT, "\"\t\t\"") << i.second << TYTI_L(charT, "\"\n"); for (const auto &i : r.childs) if (i.second) write(s, *i.second, tab + 1); s << tab << TYTI_L(charT, "}\n"); } namespace detail { template std::basic_string read_file(iStreamT &inStream) { // cache the file typedef typename iStreamT::char_type charT; std::basic_string str; inStream.seekg(0, std::ios::end); str.resize(static_cast(inStream.tellg())); if (str.empty()) return str; inStream.seekg(0, std::ios::beg); inStream.read(&str[0], static_cast(str.size())); return str; } /** \brief Read VDF formatted sequences defined by the range [first, last). If the file is mailformatted, parser will try to read it until it can. @param first begin iterator @param end end iterator @param exclude_files list of files which cant be included anymore. prevents circular includes can thow: - "std::runtime_error" if a parsing error occured - "std::bad_alloc" if not enough memory coup be allocated */ template std::vector> read_internal( IterT first, const IterT last, std::unordered_set< std::basic_string::value_type>> &exclude_files, const Options &opt) { static_assert(std::is_default_constructible::value, "Output Type must be default constructible (provide " "constructor without arguments)"); static_assert(std::is_move_constructible::value, "Output Type must be move constructible"); typedef typename std::iterator_traits::value_type charT; const std::basic_string comment_end_str = TYTI_L(charT, "*/"); const std::basic_string whitespaces = TYTI_L(charT, " \n\v\f\r\t"); #ifdef WIN32 std::function &)> is_platform_str = [](const std::basic_string &in) { return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$WINDOWS"); }; #elif __APPLE__ // WIN32 stands for pc in general std::function &)> is_platform_str = [](const std::basic_string &in) { return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || in == TYTI_L(charT, "$OSX"); }; #elif __linux__ // WIN32 stands for pc in general std::function &)> is_platform_str = [](const std::basic_string &in) { return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || in == TYTI_L(charT, "$LINUX"); }; #else std::function &)> is_platform_str = [](const std::basic_string &in) { return false; }; #endif if (opt.ignore_all_platform_conditionals) is_platform_str = [](const std::basic_string &) { return false; }; // function for skipping a comment block // iter: iterator poition to the position after a '/' auto skip_comments = [&comment_end_str](IterT iter, const IterT &last) -> IterT { ++iter; if (iter == last) return last; if (*iter == TYTI_L(charT, '/')) { // line comment, skip whole line iter = std::find(iter + 1, last, TYTI_L(charT, '\n')); if (iter == last) return last; } if (*iter == '*') { // block comment, skip until next occurance of "*\" iter = std::search(iter + 1, last, std::begin(comment_end_str), std::end(comment_end_str)); if (std::distance(iter, last) <= 2) return last; iter += 2; } return iter; }; auto end_quote = [](IterT iter, const IterT &last) -> IterT { const auto begin = iter; auto last_esc = iter; if (iter == last) throw std::runtime_error{"quote was opened but not closed."}; do { ++iter; iter = std::find(iter, last, TYTI_L(charT, '\"')); if (iter == last) break; last_esc = std::prev(iter); while (last_esc != begin && *last_esc == '\\') --last_esc; } while (!(std::distance(last_esc, iter) % 2) && iter != last); if (iter == last) throw std::runtime_error{"quote was opened but not closed."}; return iter; }; auto end_word = [&whitespaces](IterT iter, const IterT &last) -> IterT { const auto begin = iter; auto last_esc = iter; if (iter == last) throw std::runtime_error{"quote was opened but not closed."}; do { ++iter; iter = std::find_first_of(iter, last, std::begin(whitespaces), std::end(whitespaces)); if (iter == last) break; last_esc = std::prev(iter); while (last_esc != begin && *last_esc == '\\') --last_esc; } while (!(std::distance(last_esc, iter) % 2) && iter != last); if (iter == last) throw std::runtime_error{"word wasnt properly ended"}; return iter; }; auto skip_whitespaces = [&whitespaces](IterT iter, const IterT &last) -> IterT { if (iter == last) return iter; iter = std::find_if_not(iter, last, [&whitespaces](charT c) { // return true if whitespace return std::any_of(std::begin(whitespaces), std::end(whitespaces), [c](charT pc) { return pc == c; }); }); return iter; }; std::function &)> strip_escape_symbols = [](std::basic_string &s) { auto quote_searcher = [&s](size_t pos) { return s.find(TYTI_L(charT, "\\\""), pos); }; auto p = quote_searcher(0); while (p != s.npos) { s.replace(p, 2, TYTI_L(charT, "\"")); p = quote_searcher(p); } auto searcher = [&s](size_t pos) { return s.find(TYTI_L(charT, "\\\\"), pos); }; p = searcher(0); while (p != s.npos) { s.replace(p, 2, TYTI_L(charT, "\\")); p = searcher(p); } }; if (!opt.strip_escape_symbols) strip_escape_symbols = [](std::basic_string &) {}; auto conditional_fullfilled = [&skip_whitespaces, &is_platform_str](IterT &iter, const IterT &last) { iter = skip_whitespaces(iter, last); if (iter == last) return true; if (*iter == '[') { ++iter; if (iter == last) throw std::runtime_error("conditional not closed"); const auto end = std::find(iter, last, ']'); if (end == last) throw std::runtime_error("conditional not closed"); const bool negate = *iter == '!'; if (negate) ++iter; auto conditional = std::basic_string(iter, end); const bool is_platform = is_platform_str(conditional); iter = end + 1; return static_cast(is_platform ^ negate); } return true; }; // read header // first, quoted name std::unique_ptr curObj = nullptr; std::vector> roots; std::stack> lvls; auto curIter = first; while (curIter != last && *curIter != '\0') { // find first starting attrib/child, or ending curIter = skip_whitespaces(curIter, last); if (curIter == last || *curIter == '\0') break; if (*curIter == TYTI_L(charT, '/')) { curIter = skip_comments(curIter, last); if (curIter == last || *curIter == '\0') throw std::runtime_error("Unexpected eof"); } else if (*curIter != TYTI_L(charT, '}')) { // get key const auto keyEnd = (*curIter == TYTI_L(charT, '\"')) ? end_quote(curIter, last) : end_word(curIter, last); if (*curIter == TYTI_L(charT, '\"')) ++curIter; std::basic_string key(curIter, keyEnd); strip_escape_symbols(key); curIter = keyEnd + ((*keyEnd == TYTI_L(charT, '\"')) ? 1 : 0); if (curIter == last) throw std::runtime_error{"key opened, but never closed"}; curIter = skip_whitespaces(curIter, last); if (!conditional_fullfilled(curIter, last)) continue; if (curIter == last) throw std::runtime_error{"key declared, but no value"}; while (*curIter == TYTI_L(charT, '/')) { curIter = skip_comments(curIter, last); if (curIter == last || *curIter == '}') throw std::runtime_error{"key declared, but no value"}; curIter = skip_whitespaces(curIter, last); if (curIter == last || *curIter == '}') throw std::runtime_error{"key declared, but no value"}; } // get value if (*curIter != '{') { if (curIter == last) throw std::runtime_error{"key declared, but no value"}; const auto valueEnd = (*curIter == TYTI_L(charT, '\"')) ? end_quote(curIter, last) : end_word(curIter, last); if (valueEnd == last) throw std::runtime_error("No closed word"); if (*curIter == TYTI_L(charT, '\"')) ++curIter; if (curIter == last) throw std::runtime_error("No closed word"); auto value = std::basic_string(curIter, valueEnd); strip_escape_symbols(value); curIter = valueEnd + ((*valueEnd == TYTI_L(charT, '\"')) ? 1 : 0); if (!conditional_fullfilled(curIter, last)) continue; // process value if (key != TYTI_L(charT, "#include") && key != TYTI_L(charT, "#base")) { if (curObj) { curObj->add_attribute(std::move(key), std::move(value)); } else { throw std::runtime_error{ "unexpected key without object"}; } } else { if (!opt.ignore_includes && exclude_files.find(value) == exclude_files.end()) { exclude_files.insert(value); std::basic_ifstream i( detail::string_converter(value)); auto str = read_file(i); auto file_objs = read_internal( str.begin(), str.end(), exclude_files, opt); for (auto &n : file_objs) { if (curObj) curObj->add_child(std::move(n)); else roots.push_back(std::move(n)); } exclude_files.erase(value); } } } else if (*curIter == '{') { if (curObj) lvls.push(std::move(curObj)); curObj = std::make_unique(); curObj->set_name(std::move(key)); ++curIter; } } // end of new object else if (curObj && *curIter == TYTI_L(charT, '}')) { if (!lvls.empty()) { // get object before std::unique_ptr prev{std::move(lvls.top())}; lvls.pop(); // add finished obj to obj before and release it from processing prev->add_child(std::move(curObj)); curObj = std::move(prev); } else { roots.push_back(std::move(curObj)); curObj.reset(); } ++curIter; } else { throw std::runtime_error{"unexpected '}'"}; } } if (curObj != nullptr || !lvls.empty()) { throw std::runtime_error{"object is not closed with '}'"}; } return roots; } } // namespace detail /** \brief Read VDF formatted sequences defined by the range [first, last). If the file is mailformatted, parser will try to read it until it can. @param first begin iterator @param end end iterator can thow: - "std::runtime_error" if a parsing error occured - "std::bad_alloc" if not enough memory coup be allocated */ template OutputT read(IterT first, const IterT last, const Options &opt = Options{}) { auto exclude_files = std::unordered_set< std::basic_string::value_type>>{}; auto roots = detail::read_internal(first, last, exclude_files, opt); OutputT result; if (roots.size() > 1) { for (auto &i : roots) result.add_child(std::move(i)); } else if (roots.size() == 1) result = std::move(*roots[0]); return result; } /** \brief Read VDF formatted sequences defined by the range [first, last). If the file is mailformatted, parser will try to read it until it can. @param first begin iterator @param end end iterator @param ec output bool. 0 if ok, otherwise, holds an system error code Possible error codes: std::errc::protocol_error: file is mailformatted std::errc::not_enough_memory: not enough space std::errc::invalid_argument: iterators throws e.g. out of range */ template OutputT read(IterT first, IterT last, std::error_code &ec, const Options &opt = Options{}) NOEXCEPT { ec.clear(); OutputT r{}; try { r = read(first, last, opt); } catch (std::runtime_error &) { ec = std::make_error_code(std::errc::protocol_error); } catch (std::bad_alloc &) { ec = std::make_error_code(std::errc::not_enough_memory); } catch (...) { ec = std::make_error_code(std::errc::invalid_argument); } return r; } /** \brief Read VDF formatted sequences defined by the range [first, last). If the file is mailformatted, parser will try to read it until it can. @param first begin iterator @param end end iterator @param ok output bool. true, if parser successed, false, if parser failed */ template OutputT read(IterT first, const IterT last, bool *ok, const Options &opt = Options{}) NOEXCEPT { std::error_code ec; auto r = read(first, last, ec, opt); if (ok) *ok = !ec; return r; } template inline auto read(IterT first, const IterT last, bool *ok, const Options &opt = Options{}) NOEXCEPT -> basic_object::value_type> { return read::value_type>>( first, last, ok, opt); } template inline auto read(IterT first, IterT last, std::error_code &ec, const Options &opt = Options{}) NOEXCEPT -> basic_object::value_type> { return read::value_type>>( first, last, ec, opt); } template inline auto read(IterT first, const IterT last, const Options &opt = Options{}) -> basic_object::value_type> { return read::value_type>>( first, last, opt); } /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf formatted data. throws "std::bad_alloc" if file buffer could not be allocated */ template OutputT read(iStreamT &inStream, std::error_code &ec, const Options &opt = Options{}) { // cache the file typedef typename iStreamT::char_type charT; std::basic_string str = detail::read_file(inStream); // parse it return read(str.begin(), str.end(), ec, opt); } template inline basic_object read(iStreamT &inStream, std::error_code &ec, const Options &opt = Options{}) { return read>(inStream, ec, opt); } /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf formatted data. throws "std::bad_alloc" if file buffer could not be allocated ok == false, if a parsing error occured */ template OutputT read(iStreamT &inStream, bool *ok, const Options &opt = Options{}) { std::error_code ec; const auto r = read(inStream, ec, opt); if (ok) *ok = !ec; return r; } template inline basic_object read(iStreamT &inStream, bool *ok, const Options &opt = Options{}) { return read>(inStream, ok, opt); } /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf formatted data. throws "std::bad_alloc" if file buffer could not be allocated throws "std::runtime_error" if a parsing error occured */ template OutputT read(iStreamT &inStream, const Options &opt) { // cache the file typedef typename iStreamT::char_type charT; std::basic_string str = detail::read_file(inStream); // parse it return read(str.begin(), str.end(), opt); } template inline basic_object read(iStreamT &inStream, const Options &opt = Options{}) { return read>(inStream, opt); } } // namespace vdf } // namespace tyti #ifndef TYTI_NO_L_UNDEF #undef TYTI_L #endif #ifdef TYTI_UNDEF_CONSTEXPR #undef CONSTEXPR #undef TYTI_NO_L_UNDEF #endif #ifdef TYTI_UNDEF_NOTHROW #undef NOTHROW #undef TYTI_UNDEF_NOTHROW #endif #endif //__TYTI_STEAM_VDF_PARSER_H__