1 module config.util;
2 
3 import std.range.primitives;
4 import std.traits : isSomeString, isSomeChar, Unqual;
5 
6 
7 /// down cast of a reference to a child class reference
8 /// runtime check is disabled in release build
9 U unsafeCast(U, T)(T obj)
10         if ((is(T==class) || is(T==interface)) &&
11             (is(U==class) || is(U==interface)) &&
12             is(U : T))
13 in {
14     assert(obj);
15 }
16 body {
17     debug {
18         auto uObj = cast(U)obj;
19         assert(uObj, "unsafeCast from "~T.stringof~" to "~U.stringof~" failed");
20         return uObj;
21     }
22     else {
23         static if (is(T == interface) && is(U == class)) {
24             return cast(U)(cast(void*)(cast(Object)obj));
25         }
26         else {
27             return cast(U)(cast(void*)obj);
28         }
29     }
30 }
31 
32 /// ditto
33 const(U) unsafeCast(U, T)(const(T) obj)
34         if ((is(T==class) || is(T==interface)) &&
35             (is(U==class) || is(U==interface)) &&
36             is(U : T))
37 in {
38     assert(obj);
39 }
40 body {
41     debug {
42         auto uObj = cast(const(U))obj;
43         assert(uObj, "unsafeCast from "~T.stringof~" to "~U.stringof~" failed");
44         return uObj;
45     }
46     else {
47         static if (is(T == interface) && is(U == class)) {
48             return cast(const(U))(cast(const(void*))(cast(const(Object))obj));
49         }
50         else {
51             return cast(const(U))(cast(const(void*))obj);
52         }
53     }
54 }
55 
56 
57 auto findSplitAmong(alias pred="a == b", R1, R2)(R1 seq, R2 choices)
58 if (isForwardRange!R1 && isForwardRange!R2)
59 {
60     static struct Result(S1, S2, S3)
61     if (isForwardRange!S1 && isForwardRange!S2 && isForwardRange!S3)
62     {
63         import std.typecons : Tuple;
64 
65         this(S1 pre, S2 separator, S3 post)
66         {
67             asTuple = typeof(asTuple)(pre, separator, post);
68         }
69 
70         bool opCast(T : bool)()
71         {
72             return !asTuple[1].empty;
73         }
74 
75         alias asTuple this;
76         Tuple!(S1, S2, S3) asTuple;
77     }
78 
79     auto makeResult(S1, S2, S3)(S1 s1, S2 s2, S3 s3) {
80         return Result!(S1, S2, S3)(s1, s2, s3);
81     }
82 
83     static if (isSomeString!R1 && isSomeString!R2 || isRandomAccessRange!R1 && hasLength!R2)
84     {
85         import std.algorithm : findAmong;
86 
87         immutable balance = findAmong!pred(seq, choices);
88         immutable pos1 = seq.length - balance.length;
89         immutable pos2 = balance.empty ? pos1 : pos1 + 1;
90         return makeResult(seq[0 .. pos1], seq[pos1 .. pos2], seq[pos2 .. $]);
91     }
92     else
93     {
94         import std.range : takeExactly, takeOne;
95         import std.functional : binaryFun;
96 
97         auto original = seq.save;
98         size_t pos;
99 
100         seqLoop: while (!seq.empty) {
101             auto sf = seq.front;
102             auto c = choices.save;
103             while(!c.empty) {
104                 if (binaryFun!pred(sf, c.front)) {
105                     break seqLoop;
106                 }
107                 c.popFront();
108             }
109             seq.popFront();
110             ++pos;
111         }
112 
113         auto right = seq.save;
114         if (!right.empty) right.popFront();
115         return makeResult ( takeExactly(original, pos), takeOne(seq), right );
116     }
117 }
118 
119 
120 unittest {
121     {
122         auto name = "name1.name2";
123 
124         auto split1 = name.findSplitAmong(":./");
125         assert(split1[0] == "name1");
126         assert(split1[1] == ".");
127         assert(split1[2] == "name2");
128 
129         auto split2 = name.findSplitAmong(":-/");
130         assert(split2[0] == "name1.name2");
131         assert(split2[1] == "");
132         assert(split2[2] == "");
133     }
134     {
135         import std.algorithm : equal;
136 
137         static struct StrFwdRange {
138             string s;
139             @property auto empty() const { return s.length == 0; }
140             @property auto front() const { return s[0]; }
141             void popFront() { s = s[1 .. $]; }
142             @property auto save() const { return StrFwdRange(s); }
143         }
144         static assert(isForwardRange!StrFwdRange);
145 
146         auto name = StrFwdRange("name1.name2");
147 
148         auto split1 = name.findSplitAmong(StrFwdRange(":./"));
149         assert(equal(split1[0], "name1"));
150         assert(equal(split1[1], "."));
151         assert(equal(split1[2], "name2"));
152 
153         auto split2 = name.findSplitAmong(":-/");
154         assert(equal(split2[0], "name1.name2"));
155         assert(equal(split2[1], ""));
156         assert(equal(split2[2], ""));
157     }
158 }
159 
160 
161 /// strip libconfig comments on a char by char basis
162 auto stripComments(R)(R input)
163 if (isInputRange!R && isSomeChar!(ElementType!R))
164 {
165     struct Result
166     {
167         R _input;
168         ElementType!R[] _lookAhead;
169 
170         this(R input)
171         {
172             _input = input;
173             check();
174         }
175 
176         private this (R input, ElementType!R[] lookAhead)
177         {
178             _input = input;
179             _lookAhead = lookAhead;
180         }
181 
182         @property auto save()
183         {
184             return Result(_input.save, _lookAhead.dup);
185         }
186 
187         @property bool empty()
188         {
189             return _lookAhead.empty && _input.empty;
190         }
191 
192         @property auto ref front()
193         {
194             if (_lookAhead.length) return _lookAhead[0];
195             else return _input.front;
196         }
197 
198         void popFront()
199         {
200             if (_lookAhead.empty)
201             {
202                 _input.popFront();
203                 check();
204             }
205             else _lookAhead = _lookAhead[1 .. $];
206         }
207 
208         private void check()
209         {
210             assert(_lookAhead.empty);
211 
212             if (_input.empty) return;
213             else if (_input.front == '#') popUntilEol();
214             else if (_input.front == '/')
215             {
216                 immutable keep = _input.front;
217                 _input.popFront();
218 
219                 if (_input.empty) _lookAhead = [keep];
220                 else if (_input.front == '/') popUntilEol();
221                 else if (_input.front == '*') popUntilEndBlock();
222                 else _lookAhead = [keep];
223             }
224         }
225 
226         private void popUntilEol()
227         {
228             do {
229                 _input.popFront();
230             }
231             while (!_input.empty && !(_input.front == '\r' || _input.front == '\n'));
232         }
233 
234         private void popUntilEndBlock()
235         {
236             while (1) {
237                 do {
238                     _input.popFront();
239                     if (_input.empty) {
240                         throw new Exception("comment block not closed");
241                     }
242                 }
243                 while (_input.front != '*');
244 
245                 _input.popFront();
246                 if (_input.empty) {
247                     throw new Exception("comment block not closed");
248                 }
249                 if (_input.front == '/') {
250                     _input.popFront();
251                     break;
252                 }
253             }
254         }
255     }
256 
257     return Result(input);
258 }
259 
260 
261 
262 /// strip libconfig comments on a line by line basis
263 auto stripComments(R)(R input)
264 if (isInputRange!R && isSomeString!(ElementType!R))
265 {
266     alias StringT = ElementType!R;
267     alias CharT = Unqual!(typeof(StringT.init[0]));
268 
269 
270     struct Result
271     {
272         R _input;
273         StringT _buf;
274         bool _inBlock;
275         int count;
276 
277         this (R input)
278         {
279             _input = input;
280             fetch();
281         }
282 
283         this (R input, StringT buf, bool inBlock)
284         {
285             _input = input;
286             _buf = buf;
287             _inBlock = inBlock;
288         }
289 
290         static if (isForwardRange!R)
291         {
292             @property auto save()
293             {
294                 return Result(_input.save, (_buf is null) ? null : _buf.dup, _inBlock);
295             }
296         }
297 
298         @property bool empty()
299         {
300             return _buf is null;
301         }
302 
303         @property auto front()
304         {
305             return _buf;
306         }
307 
308         void popFront()
309         {
310             _buf = null;
311             if (!_input.empty) {
312                 _input.popFront();
313                 fetch();
314             }
315         }
316 
317         private void fetch()
318         {
319             import std.algorithm : find, canFind;
320 
321             if (_input.empty) return;
322 
323             auto line = _input.front;
324             assert(!line.canFind("\n"), "pass stripComments on a range of lines");
325             typeof(line) left;
326             typeof(line) right = line[];
327 
328             while (1) {
329                 immutable len = line.length;
330 
331                 if (_inBlock) {
332                     immutable end = right.find("*/");
333                     if (!end.empty) {
334                         line = left ~ end[2 .. $];
335                         _inBlock = false;
336                     }
337                     else
338                     {
339                         line = left[];
340                     }
341                 }
342                 else {
343                     immutable cmtHash = line.find("#");
344                     immutable cmtCpp = line.find("//");
345                     immutable cmtLine = cmtHash.length > cmtCpp.length ? cmtHash : cmtCpp;
346                     immutable cmtBlock = line.find("/*");
347 
348                     if (!cmtLine.empty || !cmtBlock.empty)
349                     {
350                         if (cmtBlock.length > cmtLine.length)
351                         {
352                             line = line[0 .. $-cmtBlock.length];
353                             right = cmtBlock[];
354                             _inBlock = true;
355                         }
356                         else
357                         {
358                             line = line[0 .. $-cmtLine.length];
359                         }
360                     }
361                     left = line[];
362                 }
363 
364                 if (len == line.length) break;
365             }
366 
367             _buf ~= line;
368 
369             if (_inBlock) {
370                 _input.popFront();
371                 if (_input.empty) throw new Exception("comment block not closed");
372                 else fetch();
373             }
374         }
375     }
376 
377     return Result(input);
378 }
379 
380 
381 
382 ///
383 unittest {
384     // couples of input and expected result
385     immutable text = [[
386         "some text\n"
387         "more text",
388 
389         "some text\n"
390         "more text"
391     ],
392     [
393         "// start with comments\n"
394         "some text // other comment\n"
395         "more text",
396 
397         "\n"
398         "some text \n"
399         "more text"
400     ],
401     [
402         "/ almost a comment but text\n"
403         "some text // an actual comment\n"
404         "more text",
405 
406         "/ almost a comment but text\n"
407         "some text \n"
408         "more text"
409     ],
410     [
411         "some text // comment\n"
412         "more text",
413 
414         "some text \n"
415         "more text"
416     ],
417     [
418         "some text # comment\n"
419         "more text",
420 
421         "some text \n"
422         "more text"
423     ],
424     [
425         "some text /* inlined comment */ more text",
426 
427         "some text  more text"
428     ],
429     [
430         "some text /* comment with // inlined\n"
431         "over 2 lines */ more text",
432 
433         "some text  more text"
434     ],
435     [
436         "some text // comment with /* inlined\n"
437         "more text",
438 
439         "some text \n"
440         "more text"
441     ],
442     [
443         "some text # comment with */ inlined\n"
444         "more text",
445 
446         "some text \n"
447         "more text"
448     ],
449     [
450         "some text /* multiline comment\n"
451         "still in comment\n"
452         "again comment */ more text",
453 
454         "some text  more text"
455     ]];
456 
457     foreach (t; text) {
458         import std.algorithm : equal;
459         import std.format : format;
460         import std.conv : to;
461 
462         string input = t[0];
463         string result = t[0].stripComments().to!string;
464         string expected = t[1];
465 
466         assert(equal(expected, result), format(
467             "stripComments test failed.\ninput:\n%s\nresult:\n%s\nexpected:\n%s\n",
468             input, result, expected
469         ));
470     }
471 
472     foreach (t; text) {
473         import std.algorithm : equal;
474         import std.string : lineSplitter;
475         import std.format : format;
476 
477         string [] input;
478         string [] result;
479         string [] expected;
480         foreach (s; t[0].lineSplitter) input ~= s;
481         foreach (s; t[0].lineSplitter.stripComments) result ~= s;
482         foreach (s; t[1].lineSplitter) expected ~= s;
483 
484         assert(equal(expected, result), format(
485             "lineSplitter.stripComments test failed.\ninput:\n%s\nresult:\n%s\nexpected:\n%s\n",
486             input, result, expected
487         ));
488     }
489 }
490 
491 /// Transforms the given input range by replacing lines that have @include directive
492 /// with the content of the included file. Filenames are looked for in the list
493 /// of directories includeDirs.
494 /// input must be a range of lines (as given e.g. by std.string.lineSplitter)
495 auto handleIncludeDirs(R)(R input, in string[] includeDirs=[])
496 if (isInputRange!R && isSomeString!(ElementType!R))
497 {
498     return HandleIncludeDirsResult!R(input, includeDirs);
499 }
500 
501 private struct HandleIncludeDirsResult(R)
502 {
503     IncludeHandler!(ElementType!R) hdler;
504 
505     this (R input, in string[] includeDirs)
506     {
507         hdler = makeIncludeHandler(input, includeDirs);
508     }
509 
510     @property bool empty()
511     {
512         return hdler.empty;
513     }
514     @property auto front()
515     {
516         return hdler.front;
517     }
518     void popFront()
519     {
520         hdler.popFront();
521     }
522 }
523 
524 private auto makeIncludeHandler(R) (R input, in string[] includeDirs)
525 {
526     alias StringT = typeof(input.front);
527     return cast(IncludeHandler!StringT)(new IncludeHandlerImpl!R(input, includeDirs));
528 }
529 
530 private interface IncludeHandler(StringT)
531 {
532     @property bool empty();
533     @property IncludeHandler!StringT save();
534     @property StringT front();
535     void popFront();
536 }
537 
538 private class IncludeHandlerImpl(R) : IncludeHandler!(ElementType!R)
539 {
540     alias StringT = ElementType!R;
541 
542     R _input;
543     const(string[]) _includeDirs;
544     IncludeHandler!StringT _dir;
545     bool _fstCheck;
546 
547 
548     this (R input, in string[] includeDirs)
549     {
550         _input = input;
551         _includeDirs = includeDirs;
552     }
553 
554     private this (R input, in string[] includeDirs, IncludeHandler!StringT dir, bool fstCheck)
555     {
556         _input = input;
557         _includeDirs = includeDirs;
558         _dir = dir;
559         _fstCheck = fstCheck;
560     }
561 
562     @property IncludeHandler!StringT save()
563     {
564         static if (isForwardRange!R)
565         {
566             return new IncludeHandlerImpl!R(_input.save, _includeDirs, _dir ? _dir.save : null, _fstCheck);
567         }
568         else
569         {
570             assert(false, "not implementable with input of type "~R.stringof);
571         }
572     }
573 
574     @property bool empty()
575     {
576         if (_dir) {
577             return _dir.empty && _input.empty;
578         }
579         else {
580             return _input.empty;
581         }
582     }
583 
584     @property StringT front()
585     {
586         if (!_fstCheck) check();
587         if (_dir && !_dir.empty) return _dir.front;
588         return _input.front;
589     }
590 
591     void popFront()
592     {
593         if (_dir && !_dir.empty)
594         {
595             _dir.popFront();
596             if (_dir.empty) {
597                 _dir = null;
598                 if (!_input.empty)
599                 {
600                     _input.popFront();
601                     check();
602                 }
603             }
604         }
605         else {
606             _input.popFront();
607             check();
608         }
609     }
610 
611     private void check()
612     {
613         import std.algorithm : canFind;
614         import std.path : chainPath;
615         import std.file : exists, read;
616         import std.string : lineSplitter;
617         import std.regex : ctRegex, matchFirst;
618         import std.conv : to;
619 
620         _fstCheck = true;
621 
622         if (_input.empty) return;
623         auto line = _input.front;
624         assert(!line.canFind('\n'), "handleIncludeDirs must be passed a line range");
625 
626         auto re = ctRegex!("^\\s*@include\\s*\"(.*)\"\\s*$");
627         auto m = matchFirst(line, re);
628 
629         if(m) {
630             assert(m.length >= 2);
631             auto fname = m[1];
632 
633             assert(!_dir);
634             foreach(d; _includeDirs)
635             {
636                 auto fpath = chainPath(d, fname).to!string;
637                 if (exists(fpath)) {
638                     // TODO: handle escaped chars in fname
639                     auto content = cast(StringT)read(fpath);
640                     _dir = makeIncludeHandler(content.lineSplitter, _includeDirs);
641                 }
642             }
643             if(!_dir) {
644                 throw new Exception("unresolved include directive: "~fname);
645             }
646         }
647     }
648 }
649 
650 ///
651 unittest
652 {
653     import std.file : write, readText, remove;
654 
655     scope(exit) {
656         remove("test.cfg");
657     }
658     write("test.cfg", "name=12");
659 
660     // couples of input and expected result
661     auto text = [[
662         "foo=10\nbar:30",
663         "foo=10\nbar:30"
664     ],
665     [
666         "@include \"test.cfg\"",
667         "name=12"
668     ],
669     [
670         "foo=10\n  @include \"test.cfg\" \nbar:30",
671         "foo=10\nname=12\nbar:30"
672     ],
673     [
674         "foo=10\n@include\"test.cfg\"\nbar:30",
675         "foo=10\nname=12\nbar:30"
676     ],
677     [
678         "foo=10\n@include      \"test.cfg\"\nbar:30",
679         "foo=10\nname=12\nbar:30"
680     ]];
681 
682 
683     foreach (t; text) {
684         import std.algorithm : equal;
685         import std.string : lineSplitter;
686         import std.format : format;
687 
688         string [] input;
689         string [] result;
690         string [] expected;
691         foreach (s; t[0].lineSplitter) input ~= s;
692         foreach (s; t[0].lineSplitter.handleIncludeDirs(["."])) result ~= s;
693         foreach (s; t[1].lineSplitter) expected ~= s;
694 
695         assert(equal(expected, result),
696             format("lineSplitter.handleIncludeDirs test failed.\ninput:\n%s\nresult:\n%s\nexpected:\n%s\n",
697                 input, result, expected));
698     }
699 
700 }