source: branches/tree/CORE/ConfigMapping.cxx

Last change on this file was 18716, checked in by westram, 3 years ago
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 10.0 KB
Line 
1// ========================================================= //
2//                                                           //
3//   File      : ConfigMapping.cxx                           //
4//   Purpose   : config <-> string mapping                   //
5//                                                           //
6//   Coded by Ralf Westram (coder@reallysoft.de) in Mar 19   //
7//   http://www.arb-home.de/                                 //
8//                                                           //
9// ========================================================= //
10
11#include "ConfigMapping.h"
12#include <arb_msg.h>
13
14
15using namespace std;
16
17GB_ERROR ConfigMapping::decode_escapes(string& s) {
18    string::iterator f = s.begin();
19    string::iterator t = s.begin();
20
21    for (; f != s.end(); ++f, ++t) {
22        if (*f == '\\') {
23            ++f;
24            if (f == s.end()) return GBS_global_string("Trailing \\ in '%s'", s.c_str());
25            switch (*f) {
26                case 'n':
27                    *t = '\n';
28                    break;
29                case 'r':
30                    *t = '\r';
31                    break;
32                case 't':
33                    *t = '\t';
34                    break;
35                default:
36                    *t = *f;
37                    break;
38            }
39        }
40        else {
41            *t = *f;
42        }
43    }
44
45    s.erase(t, f);
46
47    return NULp;
48}
49
50void ConfigMapping::encode_escapes(string& s, const char *to_escape) {
51    string neu;
52    neu.reserve(s.length()*2+1);
53
54    for (string::iterator p = s.begin(); p != s.end(); ++p) {
55        if (*p == '\\' || strchr(to_escape, *p)) {
56            neu = neu+'\\'+*p;
57        }
58        else if (*p == '\n') { neu = neu+"\\n"; }
59        else if (*p == '\r') { neu = neu+"\\r"; }
60        else if (*p == '\t') { neu = neu+"\\t"; }
61        else { neu = neu+*p; }
62    }
63    s = neu;
64}
65
66GB_ERROR ConfigMapping::parseFrom(const std::string& configString) {
67    // parse string in format "key1='value1';key2='value2'"..
68    // and put values into a map.
69    //
70    // assumes that keys are unique
71
72    size_t   pos         = 0;
73    GB_ERROR parse_error = NULp;
74
75    while (!parse_error) {
76        size_t equal = configString.find('=', pos);
77        if (equal == string::npos) break;
78
79        const char lookingAt = configString[equal+1];
80        if (lookingAt != '\'') {
81            parse_error = GBS_global_string("expected quote \"'\" after \"=\", found \"%c\"", lookingAt);
82            break;
83        }
84        size_t start = equal+2;
85        size_t end   = configString.find('\'', start);
86        while (end != string::npos) {
87            if (configString[end-1] != '\\') break; // unescaped quote (=end of value)
88            if (configString[end-2] == '\\') { // multiple escapes -> count
89                int    escCount = 2;
90                size_t bwd      = end-3;
91                while (bwd>=start && configString[bwd] == '\\') {
92                    escCount++;
93                    bwd--;
94                }
95                if ((escCount%2) == 0) break; // even number of escapes => unescaped quote (=end of value)
96                // otherwise the quote belongs to the value
97            }
98            end = configString.find('\'', end+1);
99        }
100        if (end == string::npos) {
101            parse_error = "could not find matching quote \"'\"";
102            break;
103        }
104
105        string config_name = configString.substr(pos, equal-pos);
106        string value       = configString.substr(start, end-start);
107
108        parse_error = decode_escapes(value);
109        if (!parse_error) {
110            set_entry(config_name, value);
111        }
112
113        pos = end+2; // skip ';'
114    }
115    return parse_error;
116}
117
118// --------------------------------------------------------------------------------
119
120#ifdef UNIT_TESTS
121#ifndef TEST_UNIT_H
122#include <test_unit.h>
123#include <arb_defs.h>
124#endif
125
126#define TEST_ESCAPE_ENCODING(plain,expected) do{                        \
127        string encoded = plain;                                         \
128        ConfigMapping::encode_escapes(encoded, "'");                    \
129        TEST_EXPECT_EQUAL(encoded, expected);                           \
130        string decoded = encoded;                                       \
131        TEST_EXPECT_NO_ERROR(ConfigMapping::decode_escapes(decoded));   \
132        TEST_EXPECT_EQUAL(decoded, plain);                              \
133    }while(0)
134
135void TEST_configValueEscaping() {
136    TEST_ESCAPE_ENCODING("hello", "hello"); // plain text
137
138    TEST_ESCAPE_ENCODING("'hello'",   "\\'hello\\'"); // quoted text
139    TEST_ESCAPE_ENCODING("\"hello\"", "\"hello\"");   // double-quoted text
140
141    // special characters (LF, CR + TAB shall not be saved to config, esp. if saving to file via AWT_config_manager)
142    TEST_ESCAPE_ENCODING("LINE\nNEXT", "LINE\\nNEXT");
143    TEST_ESCAPE_ENCODING("1\t2\r3",    "1\\t2\\r3");
144
145    // simple escape-handling
146    TEST_ESCAPE_ENCODING("hel\\lo", "hel\\\\lo");
147    TEST_ESCAPE_ENCODING("\\hello", "\\\\hello");
148    TEST_ESCAPE_ENCODING("hello\\", "hello\\\\");
149
150    TEST_ESCAPE_ENCODING("hello\\'",    "hello\\\\\\'");
151    TEST_ESCAPE_ENCODING("\\'hello\\'", "\\\\\\'hello\\\\\\'");
152
153    string invalidEncoding = "xyz\\";
154    TEST_EXPECT_ERROR_CONTAINS(ConfigMapping::decode_escapes(invalidEncoding), "Trailing \\ in 'xyz\\'");
155}
156
157struct TestedConfig {
158    const char *configString;
159    int         entryCount;
160    const char *entryList;
161};
162struct ReinterpretedConfig {
163    const char *configString;
164    const char *reinterpreted;
165};
166struct FailingConfig {
167    const char *configString;
168    GB_ERROR    error;
169};
170
171static TestedConfig testedCfg[] = {
172    { "",                      0, ""},               // empty config
173    { "tag='value'",           1, "tag" },
174    { "321='1st';abc='2nd'",   2, "321/abc" },
175    { "t-ha t='2';t;hi/s='1'", 2, "t-ha t,t;hi/s" }, // tags can contain anything but '='
176    { "t'ag'='value'",         1, "t'ag'" },         // even quotes are possible in tags
177};
178static FailingConfig failedCfg[] = {
179    { "nix=;was='ia'", "expected quote \"'\"" },          // no value
180    { "tag=value",     "expected quote \"'\"" },          // unquoted value
181    { "hasto='match",  "could not find matching quote" }, // unmatched quote
182};
183static ReinterpretedConfig reinterpretCfg[] = {
184    { "laksjd",            "" },                  // is reinterpreted as "" (empty config)
185    { "this='1';that='2'", "that='2';this='1'" }, // sorted by tagname
186};
187
188inline bool anyStringContains(const CharPtrArray& strings, char c) {
189    for (int i = 0; strings[i]; ++i) {
190        if (strchr(strings[i], c)) {
191            return true;
192        }
193    }
194    return false;
195}
196static char autodetectSeparator(const CharPtrArray& strings) {
197    const char *sep = "/;,:|#*";
198    for (int i = 0; sep[i]; ++i) {
199        if (!anyStringContains(strings, sep[i])) {
200            return sep[i];
201        }
202    }
203    TEST_REJECT(true); // add more sep
204    return 0;
205}
206
207void TEST_ConfigMapping() {
208    for (size_t c = 0; c<ARRAY_ELEMS(testedCfg); ++c) {
209        const TestedConfig& CFG = testedCfg[c];
210        TEST_ANNOTATE(CFG.configString);
211
212        ConfigMapping config;
213        TEST_EXPECT_NO_ERROR(config.parseFrom(CFG.configString));
214
215        // convert to string + compare with loaded config string:
216        TEST_EXPECT_EQUAL(config.config_string(), CFG.configString);
217
218        // test entries:
219        ConstStrArray entries;
220        config.get_entries(entries);
221        TEST_EXPECT_EQUAL(entries.size(), CFG.entryCount);
222
223        TEST_EXPECT_EQUAL_STRINGCOPY__NOERROREXPORTED(GBT_join_strings(entries, autodetectSeparator(entries)), CFG.entryList);
224    }
225
226    // test nested quoting:
227    {
228        TEST_ANNOTATE(NULp);
229        const char    *stored = "tag='val';'qtag'='\\'qval\\''";
230        ConfigMapping  config;
231        TEST_EXPECT_NO_ERROR(config.parseFrom(stored));
232
233        TEST_EXPECT_EQUAL(config.get_entry("tag"),    "val");
234        TEST_EXPECT_EQUAL(config.get_entry("'qtag'"), "'qval'");
235
236        TEST_EXPECT(config.has_entry("'qtag'"));
237        TEST_REJECT(config.has_entry("qtag"));
238
239        const char    *storedAsValue = "stored='tag=\\'val\\';\\'qtag\\'=\\'\\\\\\'qval\\\\\\'\\''";
240        ConfigMapping  wrapped;
241        wrapped.set_entry("stored", stored);
242
243        TEST_EXPECT_EQUAL(wrapped.config_string(), storedAsValue);
244
245        ConfigMapping unwrapped;
246        TEST_EXPECT_NO_ERROR(unwrapped.parseFrom(storedAsValue));
247        TEST_EXPECT_EQUAL(unwrapped.get_entry("stored"), stored);
248
249        // test delete entry
250        unwrapped.delete_entry("stored");
251        TEST_EXPECT_EQUAL(unwrapped.config_string(), "");
252
253        config.delete_entry("'qtag'");
254        TEST_EXPECT_EQUAL(config.config_string(), "tag='val'");
255
256        config.set_entry("tag", "lav"); // test overwriting a value
257        TEST_EXPECT_EQUAL(config.config_string(), "tag='lav'");
258
259        const char *SLASHED = "slashed\\";
260        config.set_entry("key", SLASHED);
261        TEST_EXPECT_EQUAL(config.config_string(), "key='slashed\\\\';tag='lav'");
262
263        {
264            ConfigMapping slashed;
265            string        cfgStr = config.config_string();
266
267            TEST_EXPECT_NO_ERROR(slashed.parseFrom(cfgStr));
268            TEST_EXPECT_EQUAL(slashed.get_entry("key"), SLASHED);
269            TEST_EXPECT_EQUAL(slashed.config_string(), cfgStr);
270        }
271    }
272
273    // test reinterpretation of configs:
274    for (size_t c = 0; c<ARRAY_ELEMS(reinterpretCfg); ++c) {
275        const ReinterpretedConfig& CFG = reinterpretCfg[c];
276        TEST_ANNOTATE(CFG.configString);
277        ConfigMapping config;
278        TEST_EXPECT_NO_ERROR(config.parseFrom(CFG.configString));
279
280        // convert to string + compare with expected reinterpretation:
281        TEST_EXPECT_EQUAL(config.config_string(), CFG.reinterpreted);
282    }
283
284    // test error-config:
285    for (size_t c = 0; c<ARRAY_ELEMS(failedCfg); ++c) {
286        const FailingConfig& CFG = failedCfg[c];
287        TEST_ANNOTATE(CFG.configString);
288        ConfigMapping config;
289        TEST_EXPECT_ERROR_CONTAINS(config.parseFrom(CFG.configString), CFG.error);
290    }
291}
292
293#endif // UNIT_TESTS
294
295// --------------------------------------------------------------------------------
296
Note: See TracBrowser for help on using the repository browser.