From 2cd52ab17ee1b830bc53321b112411122dddc1c8 Mon Sep 17 00:00:00 2001 From: Ben Longbons Date: Tue, 6 Jan 2015 17:31:21 -0800 Subject: Use Spanned while parsing config --- src/mmo/config_parse.cpp | 210 ++++++++++++++++++++++++++++++++++-------- src/mmo/config_parse.hpp | 5 +- src/mmo/config_parse_test.cpp | 60 ++++++++++++ src/mmo/version.cpp | 24 ++++- 4 files changed, 258 insertions(+), 41 deletions(-) create mode 100644 src/mmo/config_parse_test.cpp (limited to 'src/mmo') diff --git a/src/mmo/config_parse.cpp b/src/mmo/config_parse.cpp index b47ee79..6e43471 100644 --- a/src/mmo/config_parse.cpp +++ b/src/mmo/config_parse.cpp @@ -23,6 +23,8 @@ #include "../strings/xstring.hpp" #include "../strings/zstring.hpp" +#include "../compat/borrow.hpp" + #include "../io/cxxstdio.hpp" #include "../io/extract.hpp" #include "../io/line.hpp" @@ -39,40 +41,139 @@ bool is_comment(XString line) return not line or line.startswith("//"_s); } +template +static +bool do_split(io::Spanned line, io::Spanned *key, io::Spanned *value) +{ + typename ZS::iterator colon = std::find(line.data.begin(), line.data.end(), ':'); + if (colon == line.data.end()) + return false; + key->data = line.data.xislice_h(colon); + key->span = line.span; + key->span.end.column = key->span.begin.column + key->data.size() - 1; + ++colon; + value->data = line.data.xislice_t(colon); + value->span = line.span; + value->span.begin.column = value->span.end.column - value->data.size() + 1; + return true; +} + +template +static +io::Spanned do_lstrip(io::Spanned value) +{ + io::Spanned rv; + rv.data = value.data.lstrip(); + rv.span = value.span; + rv.span.begin.column += (value.data.size() - rv.data.size()); + return rv; +} + +static +io::Spanned do_rstrip(io::Spanned value) +{ + io::Spanned rv; + rv.data = value.data.rstrip(); + rv.span = value.span; + rv.span.end.column -= (value.data.size() - rv.data.size()); + return rv; +} + +static +io::Spanned do_strip(io::Spanned value) +{ + return do_lstrip(do_rstrip(value)); +} + template inline -bool config_split_impl(ZS line, XString *key, ZS *value) +bool config_split_impl(io::Spanned line, io::Spanned *key, io::Spanned *value) { // unconditionally fail if line contains control characters - if (std::find_if(line.begin(), line.end(), + if (std::find_if(line.data.begin(), line.data.end(), [](unsigned char c) { return c < ' '; } - ) != line.end()) - return false; - // try to find a colon, fail if not found - typename ZS::iterator colon = std::find(line.begin(), line.end(), ':'); - if (colon == line.end()) + ) != line.data.end()) return false; - *key = line.xislice_h(colon).strip(); - // move past the colon and any spaces - ++colon; - *value = line.xislice_t(colon).lstrip(); + if (!do_split(line, key, value)) + return false; + *key = do_strip(*key); + *value = do_lstrip(*value); return true; } // eventually this should go away -bool config_split(ZString line, XString *key, ZString *value) +// currently the only real offenders are io::FD::open and *PRINTF +bool config_split(io::Spanned line, io::Spanned *key, io::Spanned *value) { return config_split_impl(line, key, value); } -// and use this instead -bool config_split(XString line, XString *key, XString *value) + +static +bool check_version(io::Spanned avers, Borrowed valid) { - return config_split_impl(line, key, value); + enum + { + GE, LE, GT, LT + } cmp; + + if (avers.data.startswith(">="_s)) + { + cmp = GE; + avers.data = avers.data.xslice_t(2); + avers.span.begin.column += 2; + } + else if (avers.data.startswith('>')) + { + cmp = GT; + avers.data = avers.data.xslice_t(1); + avers.span.begin.column += 1; + } + else if (avers.data.startswith("<="_s)) + { + cmp = LE; + avers.data = avers.data.xslice_t(2); + avers.span.begin.column += 2; + } + else if (avers.data.startswith('<')) + { + cmp = LT; + avers.data = avers.data.xslice_t(1); + avers.span.begin.column += 1; + } + else + { + avers.span.error("Version check must begin with one of: '>=', '>', '<=', '<'"_s); + *valid = false; + return false; + } + + Version vers; + if (!extract(avers.data, &vers)) + { + avers.span.error("Bad value"_s); + *valid = false; + return false; + } + switch (cmp) + { + case GE: + return CURRENT_VERSION >= vers; + case GT: + return CURRENT_VERSION > vers; + case LE: + return CURRENT_VERSION <= vers; + case LT: + return CURRENT_VERSION < vers; + } + abort(); } /// Master config parser. This handles 'import' and 'version-ge' etc. /// Then it defers to the inferior parser for a line it does not understand. +/// +/// Note: old-style 'version-ge: 1.2.3' etc apply to the rest of the file, but +/// new-style 'version: >= 1.2.3' apply only up to the next 'version:' bool load_config_file(ZString filename, ConfigItemParser slave) { io::LineReader in(filename); @@ -81,30 +182,66 @@ bool load_config_file(ZString filename, ConfigItemParser slave) PRINTF("Unable to open file: %s\n"_fmt, filename); return false; } - io::Line line; + io::Line line_; bool rv = true; - while (in.read_line(line)) + bool good_version = true; + while (in.read_line(line_)) { - if (is_comment(line.text)) + if (is_comment(line_.text)) continue; - XString key; - ZString value; - if (!config_split(line.text, &key, &value)) + auto line = io::respan(line_.to_span(), ZString(line_.text)); + io::Spanned key; + io::Spanned value; + if (!config_split(line, &key, &value)) { - line.error("Bad config line"_s); + line.span.error("Bad config line"_s); rv = false; continue; } - if (key == "import"_s) + if (key.data == "version"_s) + { + if (value.data == "all"_s) + { + good_version = true; + } + else + { + good_version = true; + while (good_version && value.data) + { + ZString::iterator it = std::find(value.data.begin(), value.data.end(), ' '); + io::Spanned value_head; + value_head.data = value.data.xislice_h(it); + value_head.span = value.span; + value.data = value.data.xislice_t(it).lstrip(); + + value_head.span.end.column = value_head.span.begin.column + value_head.data.size() - 1; + value.span.begin.column = value.span.end.column - value.data.size() + 1; + + good_version &= check_version(value_head, borrow(rv)); + } + } + continue; + } + if (!good_version) { - rv &= load_config_file(value, slave); continue; } - else if (key == "version-lt"_s) + if (key.data == "import"_s) + { + if (!load_config_file(value.data, slave)) + { + value.span.error("Failed to include file"_s); + rv = false; + } + continue; + } + else if (key.data == "version-lt"_s) { Version vers; - if (!extract(value, &vers)) + if (!extract(value.data, &vers)) { + value.span.error("Bad value"_s); rv = false; continue; } @@ -112,47 +249,48 @@ bool load_config_file(ZString filename, ConfigItemParser slave) continue; break; } - else if (key == "version-le"_s) + else if (key.data == "version-le"_s) { Version vers; - if (!extract(value, &vers)) + if (!extract(value.data, &vers)) { rv = false; + value.span.error("Bad value"_s); continue; } if (CURRENT_VERSION <= vers) continue; break; } - else if (key == "version-gt"_s) + else if (key.data == "version-gt"_s) { Version vers; - if (!extract(value, &vers)) + if (!extract(value.data, &vers)) { rv = false; + value.span.error("Bad value"_s); continue; } if (CURRENT_VERSION > vers) continue; break; } - else if (key == "version-ge"_s) + else if (key.data == "version-ge"_s) { Version vers; - if (!extract(value, &vers)) + if (!extract(value.data, &vers)) { rv = false; + value.span.error("Bad value"_s); continue; } if (CURRENT_VERSION >= vers) continue; break; } - else if (!slave(key, value)) + else { - line.error("Bad config key or value"_s); - rv = false; - continue; + rv &= slave(key, value); } // nothing to see here, move along } diff --git a/src/mmo/config_parse.hpp b/src/mmo/config_parse.hpp index db097e9..06432ba 100644 --- a/src/mmo/config_parse.hpp +++ b/src/mmo/config_parse.hpp @@ -23,11 +23,10 @@ namespace tmwa { -typedef bool (*ConfigItemParser)(XString key, ZString value); +using ConfigItemParser = bool(io::Spanned key, io::Spanned value); bool is_comment(XString line); -bool config_split(ZString line, XString *key, ZString *value); -bool config_split(XString line, XString *key, XString *value); +bool config_split(io::Spanned line, io::Spanned *key, io::Spanned *value); /// Master config parser. This handles 'import' and 'version-ge' etc. /// Then it defers to the inferior parser for a line it does not understand. diff --git a/src/mmo/config_parse_test.cpp b/src/mmo/config_parse_test.cpp new file mode 100644 index 0000000..e1170cb --- /dev/null +++ b/src/mmo/config_parse_test.cpp @@ -0,0 +1,60 @@ +#include "config_parse.hpp" +// config_parse_test.cpp - Testsuite for config parsers +// +// Copyright © 2014 Ben Longbons +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#include "../strings/literal.hpp" +#include "../strings/rstring.hpp" + +#include "../io/span.hpp" + +#include "../poison.hpp" + + +namespace tmwa +{ +#define EXPECT_SPAN(span, bl,bc, el,ec) \ + ({ \ + EXPECT_EQ((span).begin.line, bl); \ + EXPECT_EQ((span).begin.column, bc); \ + EXPECT_EQ((span).end.line, el); \ + EXPECT_EQ((span).end.column, ec); \ + }) +TEST(configparse, keyvalue) +{ + // 0 1 2 3 + // 123456789012345678901234567890 + RString data = " key : value "_s; + + io::Spanned input, value; + io::Spanned key; + input.data = data; + input.span.begin.text = data; + input.span.begin.filename = ""_s; + input.span.begin.line = 1; + input.span.begin.column = 1; + input.span.end = input.span.begin; + input.span.end.column = data.size(); + EXPECT_EQ(data.size(), 30); + ASSERT_TRUE(config_split(input, &key, &value)); + EXPECT_SPAN(key.span, 1,3, 1,5); + EXPECT_SPAN(value.span, 1,18, 1,30); +} +} // namespace tmwa diff --git a/src/mmo/version.cpp b/src/mmo/version.cpp index 93ea794..f91b748 100644 --- a/src/mmo/version.cpp +++ b/src/mmo/version.cpp @@ -69,8 +69,28 @@ LString CURRENT_VERSION_STRING = VERSION_STRING; bool impl_extract(XString str, Version *vers) { *vers = {}; - // TODO should I try to extract dev and vend also? - // It would've been useful during the magic migration. + // versions look like: + // 1.2.3 (release) + // 1.2.3+5 (vendor patches) + // 1.2.3-4 (dev patches) + // 1.2.3-4+5 (dev patches + vendor patches) + XString a, b; + if (extract(str, record<'+'>(&a, &b))) + { + if (!extract(b, &vers->vend)) + { + return false; + } + str = a; + } + if (extract(str, record<'-'>(&a, &b))) + { + if (!extract(b, &vers->devel)) + { + return false; + } + str = a; + } return extract(str, record<'.'>(&vers->major, &vers->minor, &vers->patch)); } -- cgit v1.2.3-70-g09d2